Compare commits

..

35 commits

Author SHA1 Message Date
Kirill
da40d7df49 feat(tgbot): add title menu 2025-12-20 01:19:56 +03:00
Kirill
f045eb22b2 feat(tgbot-back): Add functions for processing user authentication 2025-12-19 17:48:16 +03:00
Kirill
d6194ec8be fix: 1 userTitle case error 2025-12-19 17:40:21 +03:00
Kirill
3bbd2c2818 Merge branch 'dev' into dev-karas
Need to get a fresh openapi description for auth
2025-12-06 05:39:18 +03:00
Kirill
dc4c430231 refactor(tgbot-front): replaced the context usage with a new thread-safe one 2025-12-06 05:05:10 +03:00
Kirill
a6848bb4d7 refactor(tgbot-front): replaced the context usage with a new thread-safe one 2025-12-06 05:02:34 +03:00
Kirill
b1c035ae35 feat(tgbot-front): start creating thread-safe user context 2025-12-06 02:44:24 +03:00
Kirill
a7b47c564a refactor(tgbot-front): small cosmetics 2025-12-06 01:59:28 +03:00
Kirill
7e0222d6f1 refactor(tgbot-front): change editMessage args 2025-12-06 01:11:34 +03:00
Kirill
19164b8d9d refactor(tgbot-front): moved the navigation handler to a separate file 2025-12-05 23:42:30 +03:00
Kirill
a22c96e7a0 build(tgbot-gen): fixing a generator error with a tag
The generator makes an error when generating tags with a link to the tag. At the moment, this is fixed by direct insertion. This is not the best solution. If a better option is found, we will fix it.
2025-12-05 23:27:02 +03:00
Kirill
20cf8b1fc2 build(tgbot-gen): Changes and additions required to generate the API client with auth (cookie)
I use a standard set of templates, changes are made in api-header.mustache and api-source.mustache. Their _old versions contain the originals. Additions are necessary for the header argument in the generated functions so that you can authenticate using cookies.
2025-12-05 22:49:14 +03:00
Kirill
ba4dfec459 refactor(tgbot): change the location of the CMakeLists.txt 2025-12-05 20:28:03 +03:00
Kirill
847aec7bdd feat(tgbot-back): start to develop back
Implemented fetchUserTitlesAsync func and embedded it in the code of the front in the trial mode. It needs to be restructured
2025-12-05 12:38:34 +03:00
Kirill
4ca8b19adb Merge branch 'dev' into dev-karas
Need to update the openapi documentation.
2025-12-05 01:42:08 +03:00
Kirill
6123ee039b feat(tgbot-front): start handleError func develop 2025-12-04 16:38:18 +03:00
Kirill
ccf9722bb7 feat(tgbot-front): implement the back button operation
Add functions to handle navigation callback logic
Also add function for creating initial user context (pay attention to auth and registration later)
2025-12-01 23:28:23 +03:00
Kirill
0fdf577612 feat(tgbot-front): add consts for titles and revs number 2025-11-28 15:22:42 +03:00
Kirill
c815e96f4c feat(tgbot-front): add new funcs to work with payload 2025-11-28 15:21:07 +03:00
Kirill
d69f5fcddf fix(tgbot-front): start fixing navigation callback processing function 2025-11-28 15:18:31 +03:00
Kirill
b368ecc43b Added context navigation logic 2025-11-28 12:57:07 +03:00
Kirill
a8dd448c95 Added a callback to go back through the state stack 2025-11-28 12:42:11 +03:00
Kirill
e09b6658b2 Bad variant of navigation handler (not working) 2025-11-28 12:30:34 +03:00
Kirill
28a7d9e691 Added payload constant 2025-11-28 12:29:53 +03:00
Kirill
12648e1a8f Changing the structure of the UserContext. Adding context history 2025-11-28 12:25:40 +03:00
Kirill
7efd7bb6b0 Added cursor field to UserContext 2025-11-27 18:13:40 +03:00
Kirill
bd309d38c6 Fixed .gitignore 2025-11-27 17:34:04 +03:00
Kirill
167e2323be Announce UserContext struct 2025-11-27 17:33:04 +03:00
Kirill
3d8abc3f0c Changed .gitignore 2025-11-27 16:24:46 +03:00
Kirill
cdc1aa2e6b Forming the BotHandlers class structure 2025-11-27 16:24:29 +03:00
Kirill
ea29fa79f0 getting ready to refactor the handlers structure 2025-11-25 22:05:12 +03:00
Kirill
45a1df4cbb Added MyTitles page passing 2025-11-25 19:45:44 +03:00
Kirill
45ce5da0ac Changed main menu text 2025-11-18 17:44:03 +03:00
Kirill
602e9b62d8 Started creating structure of bot interface 2025-11-18 17:30:43 +03:00
Kirill
879a7981cd Init commit of bot development 2025-11-17 22:15:53 +03:00
140 changed files with 6288 additions and 5467 deletions

View file

@ -100,23 +100,6 @@ jobs:
push: true push: true
tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest
- name: Build and push etl image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfiles/Dockerfile_etl
push: true
tags: meowgit.nekoea.red/nihonium/nyanimedb-etl:latest
- name: Build and push image-storage image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfiles/Dockerfile_image_storage
push: true
tags: meowgit.nekoea.red/nihonium/nyanimedb-image-storage:latest
deploy: deploy:
runs-on: debian-test runs-on: debian-test
needs: build needs: build

View file

@ -1,49 +0,0 @@
name: Build (backend build only)
on:
push:
branches:
- dev-ars
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
# Build backend
- uses: actions/setup-go@v6
with:
go-version: '^1.25'
- name: Build backend
run: |
cd modules/backend
go build -o nyanimedb .
tar -czvf nyanimedb-backend.tar.gz nyanimedb
- name: Upload built backend to artifactory
uses: actions/upload-artifact@v3
with:
name: nyanimedb-backend.tar.gz
path: modules/backend/nyanimedb-backend.tar.gz
# Build Docker images
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push backend image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfiles/Dockerfile_backend
push: true
tags: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest

View file

@ -1,54 +0,0 @@
name: Build (frontend build only)
on:
push:
branches:
- front
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
# Build frontend
- uses: actions/setup-node@v5
with:
node-version-file: modules/frontend/package.json
cache: npm
cache-dependency-path: modules/frontend/package-lock.json
- name: Build frontend
env:
VITE_BACKEND_API_BASE_URL: ${{ vars.BACKEND_API_BASE_URL }}
run: |
cd modules/frontend
npm install
npm run build
tar -czvf nyanimedb-frontend.tar.gz dist/
- name: Upload built frontend to artifactory
uses: actions/upload-artifact@v3
with:
name: nyanimedb-frontend.tar.gz
path: modules/frontend/nyanimedb-frontend.tar.gz
# Build Docker images
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push frontend image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfiles/Dockerfile_frontend
push: true
tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest

View file

@ -1,13 +0,0 @@
FROM python:3.12-slim
WORKDIR /app/modules/anime_etl
COPY modules/anime_etl/pyproject.toml modules/anime_etl/uv.lock ./
RUN pip install --no-cache-dir uv \
&& uv sync --frozen --no-dev
COPY modules/anime_etl ./
ENV NYANIMEDB_MEDIA_ROOT=/media
CMD ["uv", "run", "python", "-m", "rabbit_worker"]

View file

@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app/modules/image_storage
COPY modules/image_storage/pyproject.toml modules/image_storage/uv.lock ./
RUN pip install --no-cache-dir uv \
&& uv sync --frozen --no-dev
COPY modules/image_storage ./
ENV NYANIMEDB_MEDIA_ROOT=/media
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

1
api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
generated-client/

View file

@ -0,0 +1,59 @@
# C++ API client
{{#appDescriptionWithNewLines}}
{{{.}}}
{{/appDescriptionWithNewLines}}
## Overview
This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI spec](https://openapis.org) from a remote server, you can easily generate an API client.
- API version: {{appVersion}}
- Package version: {{packageVersion}}
{{^hideGenerationTimestamp}}
- Build date: {{generatedDate}}
{{/hideGenerationTimestamp}}
- Generator version: {{generatorVersion}}
- Build package: {{generatorClass}}
{{#infoUrl}}
For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}})
{{/infoUrl}}
- API namespace: {{{apiPackage}}}
- Model namespace: {{{modelPackage}}}
## Installation
### Prerequisites
Install [cpprestsdk](https://github.com/Microsoft/cpprestsdk).
- Windows: `vcpkg install cpprestsdk cpprestsdk:x64-windows boost-uuid boost-uuid:x64-windows`
- Mac: `brew install cpprestsdk`
- Linux: `sudo apt-get install libcpprest-dev`
### Build
```sh
cmake -DCPPREST_ROOT=/usr -DCMAKE_CXX_FLAGS="-I/usr/local/opt/openssl/include" -DCMAKE_MODULE_LINKER_FLAGS="-L/usr/local/opt/openssl/lib"
make
```
### Build on Windows with Visual Studio (VS2017)
- Right click on folder containing source code
- Select 'Open in visual studio'
- Once visual studio opens, CMake should show up in top menu bar.
- Select CMake > Build All.
*Note: If the CMake menu item doesn't show up in Visual Studio, CMake
for Visual Studio must be installed. In this case, open the 'Visual Studio
Installer' application. Select 'modify' Visual Studio 2017. Make sure
'Desktop Development with C++' is installed, and specifically that 'Visual
C++ tools for CMake' is selected in the 'Installation Details' section.
Also be sure to review the CMakeLists.txt file. Edits are likely required.*
## Author
{{#apiInfo}}{{#apis}}{{#-last}}{{infoEmail}}
{{/-last}}{{/apis}}{{/apiInfo}}

View file

@ -0,0 +1,46 @@
{{>licenseInfo}}
/*
* AnyType.h
*
* This is the implementation of an any JSON type.
*/
#ifndef {{modelHeaderGuardPrefix}}_AnyType_H_
#define {{modelHeaderGuardPrefix}}_AnyType_H_
{{{defaultInclude}}}
#include "{{packageName}}/Object.h"
#include <cpprest/details/basic_types.h>
#include <cpprest/json.h>
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
class {{declspec}} AnyType : public Object {
public:
AnyType();
virtual ~AnyType();
/////////////////////////////////////////////
/// ModelBase overrides
void validate() override;
web::json::value toJson() const override;
bool fromJson(const web::json::value &json) override;
void toMultipart(std::shared_ptr<MultipartFormData> multipart,
const utility::string_t &namePrefix) const override;
bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart,
const utility::string_t &namePrefix) override;
private:
web::json::value m_value;
};
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}
#endif /* {{modelHeaderGuardPrefix}}_AnyType_H_ */

View file

@ -0,0 +1,40 @@
{{>licenseInfo}}
#include "{{packageName}}/AnyType.h"
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
AnyType::AnyType() { m_value = web::json::value::null(); }
AnyType::~AnyType() {}
void AnyType::validate() {}
web::json::value AnyType::toJson() const { return m_value; }
bool AnyType::fromJson(const web::json::value &val) {
m_value = val;
m_IsSet = true;
return isSet();
}
void AnyType::toMultipart(std::shared_ptr<MultipartFormData> multipart,
const utility::string_t &prefix) const {
if (m_value.is_object()) {
return Object::toMultipart(multipart, prefix);
}
throw std::runtime_error("AnyType::toMultipart: unsupported type");
}
bool AnyType::fromMultiPart(std::shared_ptr<MultipartFormData> multipart,
const utility::string_t &prefix) {
if (m_value.is_object()) {
return Object::fromMultiPart(multipart, prefix);
}
return false;
}
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}

View file

@ -0,0 +1,41 @@
{{>licenseInfo}}
{{#operations}}
#ifndef {{apiHeaderGuardPrefix}}_{{classname}}GMock_H_
#define {{apiHeaderGuardPrefix}}_{{classname}}GMock_H_
#include <gmock/gmock.h>
#include "{{classname}}.h"
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
using namespace {{modelNamespace}};
class {{declspec}} {{classname}}Mock : public I{{classname}}
{
public:
using Base = I{{classname}};
{{classname}}Mock() = default;
explicit {{classname}}Mock( std::shared_ptr<ApiClient> apiClient ) { };
~{{classname}}Mock() override = default;
{{#operation}}
MOCK_METHOD{{allParams.size}}( {{operationId}}, pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> (
{{#allParams}}
{{^required}}boost::optional<{{/required}}{{#isFile}}std::shared_ptr<{{/isFile}}{{{dataType}}}{{#isFile}}>{{/isFile}}{{^required}}>{{/required}} {{paramName}}{{^-last}},{{/-last}}
{{/allParams}}
) );
{{/operation}}
};
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}
#endif /* {{apiHeaderGuardPrefix}}_{{classname}}GMock_H_ */
{{/operations}}

View file

@ -0,0 +1,86 @@
{{>licenseInfo}}
{{#operations}}/*
* {{classname}}.h
*
* {{description}}
*/
#ifndef {{apiHeaderGuardPrefix}}_{{classname}}_H_
#define {{apiHeaderGuardPrefix}}_{{classname}}_H_
{{{defaultInclude}}}
#include "{{packageName}}/ApiClient.h"
{{^hasModelImport}}#include "{{packageName}}/ModelBase.h"{{/hasModelImport}}
{{#imports}}{{{import}}}
{{/imports}}
#include <boost/optional.hpp>
#include <map> // <-- добавлено для std::map
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
using namespace {{modelNamespace}};
{{#gmockApis}}
class {{declspec}} I{{classname}}
{
public:
I{{classname}}() = default;
virtual ~I{{classname}}() = default;
{{#operation}}
virtual pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{operationId}}(
{{#allParams}}
{{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}},{{/allParams}}
const std::map<utility::string_t, utility::string_t>& customHeaders = {} // <-- добавлено
) const = 0;
{{/operation}}
};{{/gmockApis}}
class {{declspec}} {{classname}} {{#gmockApis}} : public I{{classname}} {{/gmockApis}}
{
public:
{{#gmockApis}}
using Base = I{{classname}};
{{/gmockApis}}
explicit {{classname}}( std::shared_ptr<const ApiClient> apiClient );
{{#gmockApis}}
~{{classname}}() override;
{{/gmockApis}}
{{^gmockApis}}
virtual ~{{classname}}();
{{/gmockApis}}
{{#operation}}
/// <summary>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
{{#allParams}}
/// <param name="{{paramName}}">{{#lambda.multiline_comment_4}}{{description}}{{/lambda.multiline_comment_4}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}
/// <param name="customHeaders">Additional HTTP headers to send with the request (optional)</param>
pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{operationId}}(
{{#allParams}}
{{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}},{{/allParams}}
const std::map<utility::string_t, utility::string_t>& customHeaders = {} // <-- добавлено
) const{{#gmockApis}} override{{/gmockApis}};
{{/operation}}
protected:
std::shared_ptr<const ApiClient> m_ApiClient;
};
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}
#endif /* {{apiHeaderGuardPrefix}}_{{classname}}_H_ */
{{/operations}}

View file

@ -0,0 +1,84 @@
{{>licenseInfo}}
{{#operations}}/*
* {{classname}}.h
*
* {{description}}
*/
#ifndef {{apiHeaderGuardPrefix}}_{{classname}}_H_
#define {{apiHeaderGuardPrefix}}_{{classname}}_H_
{{{defaultInclude}}}
#include "{{packageName}}/ApiClient.h"
{{^hasModelImport}}#include "{{packageName}}/ModelBase.h"{{/hasModelImport}}
{{#imports}}{{{import}}}
{{/imports}}
#include <boost/optional.hpp>
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
using namespace {{modelNamespace}};
{{#gmockApis}}
class {{declspec}} I{{classname}}
{
public:
I{{classname}}() = default;
virtual ~I{{classname}}() = default;
{{#operation}}
virtual pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{operationId}}(
{{#allParams}}
{{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}}{{^-last}},{{/-last}}
{{/allParams}}
) const = 0;
{{/operation}}
};{{/gmockApis}}
class {{declspec}} {{classname}} {{#gmockApis}} : public I{{classname}} {{/gmockApis}}
{
public:
{{#gmockApis}}
using Base = I{{classname}};
{{/gmockApis}}
explicit {{classname}}( std::shared_ptr<const ApiClient> apiClient );
{{#gmockApis}}
~{{classname}}() override;
{{/gmockApis}}
{{^gmockApis}}
virtual ~{{classname}}();
{{/gmockApis}}
{{#operation}}
/// <summary>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
{{#allParams}}
/// <param name="{{paramName}}">{{#lambda.multiline_comment_4}}{{description}}{{/lambda.multiline_comment_4}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}
pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{operationId}}(
{{#allParams}}
{{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}}{{^-last}},{{/-last}}
{{/allParams}}
) const{{#gmockApis}} override{{/gmockApis}};
{{/operation}}
protected:
std::shared_ptr<const ApiClient> m_ApiClient;
};
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}
#endif /* {{apiHeaderGuardPrefix}}_{{classname}}_H_ */
{{/operations}}

View file

@ -0,0 +1,375 @@
{{>licenseInfo}}
{{#operations}}
#include "{{packageName}}/api/{{classname}}.h"
#include "{{packageName}}/IHttpBody.h"
#include "{{packageName}}/JsonBody.h"
#include "{{packageName}}/MultipartFormData.h"
#include <boost/algorithm/string/replace.hpp>
#include <unordered_set>
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
using namespace {{modelNamespace}};
{{classname}}::{{classname}}( std::shared_ptr<const ApiClient> apiClient )
: m_ApiClient(apiClient)
{
}
{{classname}}::~{{classname}}()
{
}
{{#operation}}
pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{classname}}::{{operationId}}({{#allParams}}{{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}}, {{/allParams}}const std::map<utility::string_t, utility::string_t>& customHeaders) const // <-- изменено: все параметры с запятой, + customHeaders
{
{{#allParams}}{{#required}}{{^isPrimitiveType}}{{^isContainer}}
// verify the required parameter '{{paramName}}' is set
if ({{paramName}} == nullptr)
{
throw ApiException(400, utility::conversions::to_string_t("Missing required parameter '{{paramName}}' when calling {{classname}}->{{operationId}}"));
}
{{/isContainer}}{{/isPrimitiveType}}{{/required}}{{/allParams}}
std::shared_ptr<const ApiConfiguration> localVarApiConfiguration( m_ApiClient->getConfiguration() );
utility::string_t localVarPath = utility::conversions::to_string_t("{{{path}}}");
{{#pathParams}}
boost::replace_all(localVarPath, utility::conversions::to_string_t("{") + utility::conversions::to_string_t("{{baseName}}") + utility::conversions::to_string_t("}"), web::uri::encode_uri(ApiClient::parameterToString({{{paramName}}})));
{{/pathParams}}
std::map<utility::string_t, utility::string_t> localVarQueryParams;
std::map<utility::string_t, utility::string_t> localVarHeaderParams( localVarApiConfiguration->getDefaultHeaders() ); // <-- уже содержит defaultHeaders
std::map<utility::string_t, utility::string_t> localVarFormParams;
std::map<utility::string_t, std::shared_ptr<HttpContent>> localVarFileParams;
std::unordered_set<utility::string_t> localVarResponseHttpContentTypes;
{{#produces}}
localVarResponseHttpContentTypes.insert( utility::conversions::to_string_t("{{{mediaType}}}") );
{{/produces}}
utility::string_t localVarResponseHttpContentType;
// use JSON if possible
if ( localVarResponseHttpContentTypes.size() == 0 )
{
{{#vendorExtensions.x-codegen-response.isString}}
localVarResponseHttpContentType = utility::conversions::to_string_t("text/plain");
{{/vendorExtensions.x-codegen-response.isString}}
{{^vendorExtensions.x-codegen-response.isString}}
localVarResponseHttpContentType = utility::conversions::to_string_t("application/json");
{{/vendorExtensions.x-codegen-response.isString}}
}
// JSON
else if ( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("application/json")) != localVarResponseHttpContentTypes.end() )
{
localVarResponseHttpContentType = utility::conversions::to_string_t("application/json");
}
// multipart formdata
else if( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("multipart/form-data")) != localVarResponseHttpContentTypes.end() )
{
localVarResponseHttpContentType = utility::conversions::to_string_t("multipart/form-data");
}
{{#vendorExtensions.x-codegen-response.isString}}
// plain text
else if( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("text/plain")) != localVarResponseHttpContentTypes.end() )
{
localVarResponseHttpContentType = utility::conversions::to_string_t("text/plain");
}
{{/vendorExtensions.x-codegen-response.isString}}
{{#vendorExtensions.x-codegen-response-ishttpcontent}}
else
{
//It's going to be binary, so just use the first one.
localVarResponseHttpContentType = *localVarResponseHttpContentTypes.begin();
}
{{/vendorExtensions.x-codegen-response-ishttpcontent}}
{{^vendorExtensions.x-codegen-response-ishttpcontent}}
else
{
throw ApiException(400, utility::conversions::to_string_t("{{classname}}->{{operationId}} does not produce any supported media type"));
}
{{/vendorExtensions.x-codegen-response-ishttpcontent}}
localVarHeaderParams[utility::conversions::to_string_t("Accept")] = localVarResponseHttpContentType;
std::unordered_set<utility::string_t> localVarConsumeHttpContentTypes;
{{#consumes}}
localVarConsumeHttpContentTypes.insert( utility::conversions::to_string_t("{{{mediaType}}}") );
{{/consumes}}
{{#allParams}}
{{^isBodyParam}}
{{^isPathParam}}
{{#required}}
{{^isPrimitiveType}}
{{^isContainer}}
if ({{paramName}} != nullptr)
{{/isContainer}}
{{/isPrimitiveType}}
{{/required}}
{{^required}}
{{^isPrimitiveType}}
{{^isContainer}}
if ({{paramName}} && *{{paramName}} != nullptr)
{{/isContainer}}
{{/isPrimitiveType}}
{{#isPrimitiveType}}
{{#isFile}}
if ({{paramName}} && *{{paramName}} != nullptr)
{{/isFile}}
{{^isFile}}
if ({{paramName}})
{{/isFile}}
{{/isPrimitiveType}}
{{#isContainer}}
if ({{paramName}})
{{/isContainer}}
{{/required}}
{
{{#isQueryParam}}
localVarQueryParams[utility::conversions::to_string_t("{{baseName}}")] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}});
{{/isQueryParam}}
{{#isHeaderParam}}
localVarHeaderParams[utility::conversions::to_string_t("{{baseName}}")] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}});
{{/isHeaderParam}}
{{#isFormParam}}
{{#isFile}}
localVarFileParams[ utility::conversions::to_string_t("{{baseName}}") ] = {{^required}}*{{/required}}{{paramName}};
{{/isFile}}
{{^isFile}}
localVarFormParams[ utility::conversions::to_string_t("{{baseName}}") ] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}});
{{/isFile}}
{{/isFormParam}}
}
{{/isPathParam}}
{{/isBodyParam}}
{{/allParams}}
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
// Apply customHeaders AFTER all other headers (so they can override)
for (const auto& header : customHeaders) {
localVarHeaderParams[header.first] = header.second;
}
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
std::shared_ptr<IHttpBody> localVarHttpBody;
utility::string_t localVarRequestHttpContentType;
// use JSON if possible
if ( localVarConsumeHttpContentTypes.size() == 0 || localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("application/json")) != localVarConsumeHttpContentTypes.end() )
{
localVarRequestHttpContentType = utility::conversions::to_string_t("application/json");
{{#bodyParam}}
web::json::value localVarJson;
{{#isPrimitiveType}}
localVarJson = ModelBase::toJson({{paramName}}{{^required}}.get(){{/required}});
{{/isPrimitiveType}}
{{^isPrimitiveType}}
{{#isArray}}
{
std::vector<web::json::value> localVarJsonArray;
for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} )
{
{{#items.isPrimitiveType}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem));
{{/items.isPrimitiveType}}{{^items.isPrimitiveType}}{{#items.isString}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem));
{{/items.isString}}{{^items.isString}}{{#items.isDateTime}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem));
{{/items.isDateTime}}{{^items.isDateTime}}localVarJsonArray.push_back( localVarItem.get() ? localVarItem->toJson() : web::json::value::null() );
{{/items.isDateTime}}{{/items.isString}}{{/items.isPrimitiveType}}
}
localVarJson = web::json::value::array(localVarJsonArray);
}
{{/isArray}}
{{^isArray}}{{#required}}localVarJson = ModelBase::toJson({{paramName}});
{{/required}}{{^required}}if ({{paramName}})
localVarJson = ModelBase::toJson(*{{paramName}});{{/required}}
{{/isArray}}
{{/isPrimitiveType}}
localVarHttpBody = std::shared_ptr<IHttpBody>( new JsonBody( localVarJson ) );
{{/bodyParam}}
}
// multipart formdata
else if( localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("multipart/form-data")) != localVarConsumeHttpContentTypes.end() )
{
localVarRequestHttpContentType = utility::conversions::to_string_t("multipart/form-data");
{{#bodyParam}}
std::shared_ptr<MultipartFormData> localVarMultipart(new MultipartFormData);
{{#isPrimitiveType}}
localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), {{paramName}}{{^required}}.get(){{/required}}));
{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isArray}}
{
std::vector<web::json::value> localVarJsonArray;
for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} )
{
localVarJsonArray.push_back(ModelBase::toJson(localVarItem));
}
localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), localVarJsonArray, utility::conversions::to_string_t("application/json")));
}{{/isArray}}{{#isMap}}
{
std::map<utility::string_t, web::json::value> localVarJsonMap;
for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} )
{
web::json::value jval;
localVarJsonMap.insert( std::pair<utility::string_t, web::json::value>(localVarItem.first, ModelBase::toJson(localVarItem.second) ));
}
localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), localVarJsonMap, utility::conversions::to_string_t("application/json")));
}{{/isMap}}
{{^isArray}}{{^isMap}}{{#isString}}localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), {{paramName}}));
{{/isString}}{{^isString}}if({{^required}}{{paramName}} && (*{{paramName}}){{/required}}{{#required}}{{paramName}}{{/required}}.get())
{
{{^required}}(*{{/required}}{{paramName}}{{^required}}){{/required}}->toMultipart(localVarMultipart, utility::conversions::to_string_t("{{paramName}}"));
}
{{/isString}}
{{/isMap}}{{/isArray}}{{/isPrimitiveType}}
localVarHttpBody = localVarMultipart;
localVarRequestHttpContentType += utility::conversions::to_string_t("; boundary=") + localVarMultipart->getBoundary();
{{/bodyParam}}
}
else if (localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("application/x-www-form-urlencoded")) != localVarConsumeHttpContentTypes.end())
{
localVarRequestHttpContentType = utility::conversions::to_string_t("application/x-www-form-urlencoded");
}
else
{
throw ApiException(415, utility::conversions::to_string_t("{{classname}}->{{operationId}} does not consume any supported media type"));
}
{{#authMethods}}
// authentication ({{name}}) required
{{#isApiKey}}
{{#isKeyInHeader}}
{
utility::string_t localVarApiKey = localVarApiConfiguration->getApiKey(utility::conversions::to_string_t("{{keyParamName}}"));
if ( localVarApiKey.size() > 0 )
{
localVarHeaderParams[utility::conversions::to_string_t("{{keyParamName}}")] = localVarApiKey;
}
}
{{/isKeyInHeader}}
{{#isKeyInQuery}}
{
utility::string_t localVarApiKey = localVarApiConfiguration->getApiKey(utility::conversions::to_string_t("{{keyParamName}}"));
if ( localVarApiKey.size() > 0 )
{
localVarQueryParams[utility::conversions::to_string_t("{{keyParamName}}")] = localVarApiKey;
}
}
{{/isKeyInQuery}}
{{/isApiKey}}
{{#isBasicBasic}}
// Basic authentication is added automatically as part of the http_client_config
{{/isBasicBasic}}
{{#isOAuth}}
// oauth2 authentication is added automatically as part of the http_client_config
{{/isOAuth}}
{{/authMethods}}
return m_ApiClient->callApi(localVarPath, utility::conversions::to_string_t("{{httpMethod}}"), localVarQueryParams, localVarHttpBody, localVarHeaderParams, localVarFormParams, localVarFileParams, localVarRequestHttpContentType)
.then([=, this](web::http::http_response localVarResponse)
{
if (m_ApiClient->getResponseHandler())
{
m_ApiClient->getResponseHandler()(localVarResponse.status_code(), localVarResponse.headers());
}
// 1xx - informational : OK
// 2xx - successful : OK
// 3xx - redirection : OK
// 4xx - client error : not OK
// 5xx - client error : not OK
if (localVarResponse.status_code() >= 400)
{
throw ApiException(localVarResponse.status_code()
, utility::conversions::to_string_t("error calling {{operationId}}: ") + localVarResponse.reason_phrase()
, std::make_shared<std::stringstream>(localVarResponse.extract_utf8string(true).get()));
}
// check response content type
if(localVarResponse.headers().has(utility::conversions::to_string_t("Content-Type")))
{
utility::string_t localVarContentType = localVarResponse.headers()[utility::conversions::to_string_t("Content-Type")];
if( localVarContentType.find(localVarResponseHttpContentType) == std::string::npos )
{
throw ApiException(500
, utility::conversions::to_string_t("error calling {{operationId}}: unexpected response type: ") + localVarContentType
, std::make_shared<std::stringstream>(localVarResponse.extract_utf8string(true).get()));
}
}
{{#vendorExtensions.x-codegen-response-ishttpcontent}}
return localVarResponse.extract_vector();
})
.then([=, this](std::vector<unsigned char> localVarResponse)
{
{{{returnType}}} localVarResult = std::make_shared<HttpContent>();
std::shared_ptr<std::stringstream> stream = std::make_shared<std::stringstream>(std::string(localVarResponse.begin(), localVarResponse.end()));
localVarResult->setData(stream);
return localVarResult;
{{/vendorExtensions.x-codegen-response-ishttpcontent}}
{{^vendorExtensions.x-codegen-response-ishttpcontent}}
return localVarResponse.extract_string();
})
.then([=, this](utility::string_t localVarResponse)
{
{{^returnType}}
return void();
{{/returnType}}
{{#returnType}}
{{#returnContainer}}
{{{returnType}}} localVarResult;
{{/returnContainer}}
{{^returnContainer}}
{{{returnType}}} localVarResult({{{defaultResponse}}});
{{/returnContainer}}
if(localVarResponseHttpContentType == utility::conversions::to_string_t("application/json"))
{
web::json::value localVarJson = web::json::value::parse(localVarResponse);
{{#isArray}}
for( auto& localVarItem : localVarJson.as_array() )
{
{{{vendorExtensions.x-codegen-response.items.datatype}}} localVarItemObj;
ModelBase::fromJson(localVarItem, localVarItemObj);
localVarResult.push_back(localVarItemObj);
}{{/isArray}}{{#isMap}}
for( auto& localVarItem : localVarJson.as_object() )
{
{{{vendorExtensions.x-codegen-response.items.datatype}}} localVarItemObj;
ModelBase::fromJson(localVarItem.second, localVarItemObj);
localVarResult[localVarItem.first] = localVarItemObj;
}{{/isMap}}{{^isArray}}{{^isMap}}
ModelBase::fromJson(localVarJson, localVarResult);{{/isMap}}{{/isArray}}
}{{#vendorExtensions.x-codegen-response.isString}}
else if(localVarResponseHttpContentType == utility::conversions::to_string_t("text/plain"))
{
localVarResult = localVarResponse;
}{{/vendorExtensions.x-codegen-response.isString}}
// else if(localVarResponseHttpContentType == utility::conversions::to_string_t("multipart/form-data"))
// {
// TODO multipart response parsing
// }
else
{
throw ApiException(500
, utility::conversions::to_string_t("error calling {{operationId}}: unsupported response type"));
}
return localVarResult;
{{/returnType}}
{{/vendorExtensions.x-codegen-response-ishttpcontent}}
});
}
{{/operation}}
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}
{{/operations}}

View file

@ -0,0 +1,368 @@
{{>licenseInfo}}
{{#operations}}
#include "{{packageName}}/api/{{classname}}.h"
#include "{{packageName}}/IHttpBody.h"
#include "{{packageName}}/JsonBody.h"
#include "{{packageName}}/MultipartFormData.h"
#include <boost/algorithm/string/replace.hpp>
#include <unordered_set>
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
using namespace {{modelNamespace}};
{{classname}}::{{classname}}( std::shared_ptr<const ApiClient> apiClient )
: m_ApiClient(apiClient)
{
}
{{classname}}::~{{classname}}()
{
}
{{#operation}}
pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{classname}}::{{operationId}}({{#allParams}}{{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) const
{
{{#allParams}}{{#required}}{{^isPrimitiveType}}{{^isContainer}}
// verify the required parameter '{{paramName}}' is set
if ({{paramName}} == nullptr)
{
throw ApiException(400, utility::conversions::to_string_t("Missing required parameter '{{paramName}}' when calling {{classname}}->{{operationId}}"));
}
{{/isContainer}}{{/isPrimitiveType}}{{/required}}{{/allParams}}
std::shared_ptr<const ApiConfiguration> localVarApiConfiguration( m_ApiClient->getConfiguration() );
utility::string_t localVarPath = utility::conversions::to_string_t("{{{path}}}");
{{#pathParams}}
boost::replace_all(localVarPath, utility::conversions::to_string_t("{") + utility::conversions::to_string_t("{{baseName}}") + utility::conversions::to_string_t("}"), web::uri::encode_uri(ApiClient::parameterToString({{{paramName}}})));
{{/pathParams}}
std::map<utility::string_t, utility::string_t> localVarQueryParams;
std::map<utility::string_t, utility::string_t> localVarHeaderParams( localVarApiConfiguration->getDefaultHeaders() );
std::map<utility::string_t, utility::string_t> localVarFormParams;
std::map<utility::string_t, std::shared_ptr<HttpContent>> localVarFileParams;
std::unordered_set<utility::string_t> localVarResponseHttpContentTypes;
{{#produces}}
localVarResponseHttpContentTypes.insert( utility::conversions::to_string_t("{{{mediaType}}}") );
{{/produces}}
utility::string_t localVarResponseHttpContentType;
// use JSON if possible
if ( localVarResponseHttpContentTypes.size() == 0 )
{
{{#vendorExtensions.x-codegen-response.isString}}
localVarResponseHttpContentType = utility::conversions::to_string_t("text/plain");
{{/vendorExtensions.x-codegen-response.isString}}
{{^vendorExtensions.x-codegen-response.isString}}
localVarResponseHttpContentType = utility::conversions::to_string_t("application/json");
{{/vendorExtensions.x-codegen-response.isString}}
}
// JSON
else if ( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("application/json")) != localVarResponseHttpContentTypes.end() )
{
localVarResponseHttpContentType = utility::conversions::to_string_t("application/json");
}
// multipart formdata
else if( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("multipart/form-data")) != localVarResponseHttpContentTypes.end() )
{
localVarResponseHttpContentType = utility::conversions::to_string_t("multipart/form-data");
}
{{#vendorExtensions.x-codegen-response.isString}}
// plain text
else if( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("text/plain")) != localVarResponseHttpContentTypes.end() )
{
localVarResponseHttpContentType = utility::conversions::to_string_t("text/plain");
}
{{/vendorExtensions.x-codegen-response.isString}}
{{#vendorExtensions.x-codegen-response-ishttpcontent}}
else
{
//It's going to be binary, so just use the first one.
localVarResponseHttpContentType = *localVarResponseHttpContentTypes.begin();
}
{{/vendorExtensions.x-codegen-response-ishttpcontent}}
{{^vendorExtensions.x-codegen-response-ishttpcontent}}
else
{
throw ApiException(400, utility::conversions::to_string_t("{{classname}}->{{operationId}} does not produce any supported media type"));
}
{{/vendorExtensions.x-codegen-response-ishttpcontent}}
localVarHeaderParams[utility::conversions::to_string_t("Accept")] = localVarResponseHttpContentType;
std::unordered_set<utility::string_t> localVarConsumeHttpContentTypes;
{{#consumes}}
localVarConsumeHttpContentTypes.insert( utility::conversions::to_string_t("{{{mediaType}}}") );
{{/consumes}}
{{#allParams}}
{{^isBodyParam}}
{{^isPathParam}}
{{#required}}
{{^isPrimitiveType}}
{{^isContainer}}
if ({{paramName}} != nullptr)
{{/isContainer}}
{{/isPrimitiveType}}
{{/required}}
{{^required}}
{{^isPrimitiveType}}
{{^isContainer}}
if ({{paramName}} && *{{paramName}} != nullptr)
{{/isContainer}}
{{/isPrimitiveType}}
{{#isPrimitiveType}}
{{#isFile}}
if ({{paramName}} && *{{paramName}} != nullptr)
{{/isFile}}
{{^isFile}}
if ({{paramName}})
{{/isFile}}
{{/isPrimitiveType}}
{{#isContainer}}
if ({{paramName}})
{{/isContainer}}
{{/required}}
{
{{#isQueryParam}}
localVarQueryParams[utility::conversions::to_string_t("{{baseName}}")] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}});
{{/isQueryParam}}
{{#isHeaderParam}}
localVarHeaderParams[utility::conversions::to_string_t("{{baseName}}")] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}});
{{/isHeaderParam}}
{{#isFormParam}}
{{#isFile}}
localVarFileParams[ utility::conversions::to_string_t("{{baseName}}") ] = {{^required}}*{{/required}}{{paramName}};
{{/isFile}}
{{^isFile}}
localVarFormParams[ utility::conversions::to_string_t("{{baseName}}") ] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}});
{{/isFile}}
{{/isFormParam}}
}
{{/isPathParam}}
{{/isBodyParam}}
{{/allParams}}
std::shared_ptr<IHttpBody> localVarHttpBody;
utility::string_t localVarRequestHttpContentType;
// use JSON if possible
if ( localVarConsumeHttpContentTypes.size() == 0 || localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("application/json")) != localVarConsumeHttpContentTypes.end() )
{
localVarRequestHttpContentType = utility::conversions::to_string_t("application/json");
{{#bodyParam}}
web::json::value localVarJson;
{{#isPrimitiveType}}
localVarJson = ModelBase::toJson({{paramName}}{{^required}}.get(){{/required}});
{{/isPrimitiveType}}
{{^isPrimitiveType}}
{{#isArray}}
{
std::vector<web::json::value> localVarJsonArray;
for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} )
{
{{#items.isPrimitiveType}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem));
{{/items.isPrimitiveType}}{{^items.isPrimitiveType}}{{#items.isString}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem));
{{/items.isString}}{{^items.isString}}{{#items.isDateTime}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem));
{{/items.isDateTime}}{{^items.isDateTime}}localVarJsonArray.push_back( localVarItem.get() ? localVarItem->toJson() : web::json::value::null() );
{{/items.isDateTime}}{{/items.isString}}{{/items.isPrimitiveType}}
}
localVarJson = web::json::value::array(localVarJsonArray);
}
{{/isArray}}
{{^isArray}}{{#required}}localVarJson = ModelBase::toJson({{paramName}});
{{/required}}{{^required}}if ({{paramName}})
localVarJson = ModelBase::toJson(*{{paramName}});{{/required}}
{{/isArray}}
{{/isPrimitiveType}}
localVarHttpBody = std::shared_ptr<IHttpBody>( new JsonBody( localVarJson ) );
{{/bodyParam}}
}
// multipart formdata
else if( localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("multipart/form-data")) != localVarConsumeHttpContentTypes.end() )
{
localVarRequestHttpContentType = utility::conversions::to_string_t("multipart/form-data");
{{#bodyParam}}
std::shared_ptr<MultipartFormData> localVarMultipart(new MultipartFormData);
{{#isPrimitiveType}}
localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), {{paramName}}{{^required}}.get(){{/required}}));
{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isArray}}
{
std::vector<web::json::value> localVarJsonArray;
for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} )
{
localVarJsonArray.push_back(ModelBase::toJson(localVarItem));
}
localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), localVarJsonArray, utility::conversions::to_string_t("application/json")));
}{{/isArray}}{{#isMap}}
{
std::map<utility::string_t, web::json::value> localVarJsonMap;
for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} )
{
web::json::value jval;
localVarJsonMap.insert( std::pair<utility::string_t, web::json::value>(localVarItem.first, ModelBase::toJson(localVarItem.second) ));
}
localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), localVarJsonMap, utility::conversions::to_string_t("application/json")));
}{{/isMap}}
{{^isArray}}{{^isMap}}{{#isString}}localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), {{paramName}}));
{{/isString}}{{^isString}}if({{^required}}{{paramName}} && (*{{paramName}}){{/required}}{{#required}}{{paramName}}{{/required}}.get())
{
{{^required}}(*{{/required}}{{paramName}}{{^required}}){{/required}}->toMultipart(localVarMultipart, utility::conversions::to_string_t("{{paramName}}"));
}
{{/isString}}
{{/isMap}}{{/isArray}}{{/isPrimitiveType}}
localVarHttpBody = localVarMultipart;
localVarRequestHttpContentType += utility::conversions::to_string_t("; boundary=") + localVarMultipart->getBoundary();
{{/bodyParam}}
}
else if (localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("application/x-www-form-urlencoded")) != localVarConsumeHttpContentTypes.end())
{
localVarRequestHttpContentType = utility::conversions::to_string_t("application/x-www-form-urlencoded");
}
else
{
throw ApiException(415, utility::conversions::to_string_t("{{classname}}->{{operationId}} does not consume any supported media type"));
}
{{#authMethods}}
// authentication ({{name}}) required
{{#isApiKey}}
{{#isKeyInHeader}}
{
utility::string_t localVarApiKey = localVarApiConfiguration->getApiKey(utility::conversions::to_string_t("{{keyParamName}}"));
if ( localVarApiKey.size() > 0 )
{
localVarHeaderParams[utility::conversions::to_string_t("{{keyParamName}}")] = localVarApiKey;
}
}
{{/isKeyInHeader}}
{{#isKeyInQuery}}
{
utility::string_t localVarApiKey = localVarApiConfiguration->getApiKey(utility::conversions::to_string_t("{{keyParamName}}"));
if ( localVarApiKey.size() > 0 )
{
localVarQueryParams[utility::conversions::to_string_t("{{keyParamName}}")] = localVarApiKey;
}
}
{{/isKeyInQuery}}
{{/isApiKey}}
{{#isBasicBasic}}
// Basic authentication is added automatically as part of the http_client_config
{{/isBasicBasic}}
{{#isOAuth}}
// oauth2 authentication is added automatically as part of the http_client_config
{{/isOAuth}}
{{/authMethods}}
return m_ApiClient->callApi(localVarPath, utility::conversions::to_string_t("{{httpMethod}}"), localVarQueryParams, localVarHttpBody, localVarHeaderParams, localVarFormParams, localVarFileParams, localVarRequestHttpContentType)
.then([=, this](web::http::http_response localVarResponse)
{
if (m_ApiClient->getResponseHandler())
{
m_ApiClient->getResponseHandler()(localVarResponse.status_code(), localVarResponse.headers());
}
// 1xx - informational : OK
// 2xx - successful : OK
// 3xx - redirection : OK
// 4xx - client error : not OK
// 5xx - client error : not OK
if (localVarResponse.status_code() >= 400)
{
throw ApiException(localVarResponse.status_code()
, utility::conversions::to_string_t("error calling {{operationId}}: ") + localVarResponse.reason_phrase()
, std::make_shared<std::stringstream>(localVarResponse.extract_utf8string(true).get()));
}
// check response content type
if(localVarResponse.headers().has(utility::conversions::to_string_t("Content-Type")))
{
utility::string_t localVarContentType = localVarResponse.headers()[utility::conversions::to_string_t("Content-Type")];
if( localVarContentType.find(localVarResponseHttpContentType) == std::string::npos )
{
throw ApiException(500
, utility::conversions::to_string_t("error calling {{operationId}}: unexpected response type: ") + localVarContentType
, std::make_shared<std::stringstream>(localVarResponse.extract_utf8string(true).get()));
}
}
{{#vendorExtensions.x-codegen-response-ishttpcontent}}
return localVarResponse.extract_vector();
})
.then([=, this](std::vector<unsigned char> localVarResponse)
{
{{{returnType}}} localVarResult = std::make_shared<HttpContent>();
std::shared_ptr<std::stringstream> stream = std::make_shared<std::stringstream>(std::string(localVarResponse.begin(), localVarResponse.end()));
localVarResult->setData(stream);
return localVarResult;
{{/vendorExtensions.x-codegen-response-ishttpcontent}}
{{^vendorExtensions.x-codegen-response-ishttpcontent}}
return localVarResponse.extract_string();
})
.then([=, this](utility::string_t localVarResponse)
{
{{^returnType}}
return void();
{{/returnType}}
{{#returnType}}
{{#returnContainer}}
{{{returnType}}} localVarResult;
{{/returnContainer}}
{{^returnContainer}}
{{{returnType}}} localVarResult({{{defaultResponse}}});
{{/returnContainer}}
if(localVarResponseHttpContentType == utility::conversions::to_string_t("application/json"))
{
web::json::value localVarJson = web::json::value::parse(localVarResponse);
{{#isArray}}
for( auto& localVarItem : localVarJson.as_array() )
{
{{{vendorExtensions.x-codegen-response.items.datatype}}} localVarItemObj;
ModelBase::fromJson(localVarItem, localVarItemObj);
localVarResult.push_back(localVarItemObj);
}{{/isArray}}{{#isMap}}
for( auto& localVarItem : localVarJson.as_object() )
{
{{{vendorExtensions.x-codegen-response.items.datatype}}} localVarItemObj;
ModelBase::fromJson(localVarItem.second, localVarItemObj);
localVarResult[localVarItem.first] = localVarItemObj;
}{{/isMap}}{{^isArray}}{{^isMap}}
ModelBase::fromJson(localVarJson, localVarResult);{{/isMap}}{{/isArray}}
}{{#vendorExtensions.x-codegen-response.isString}}
else if(localVarResponseHttpContentType == utility::conversions::to_string_t("text/plain"))
{
localVarResult = localVarResponse;
}{{/vendorExtensions.x-codegen-response.isString}}
// else if(localVarResponseHttpContentType == utility::conversions::to_string_t("multipart/form-data"))
// {
// TODO multipart response parsing
// }
else
{
throw ApiException(500
, utility::conversions::to_string_t("error calling {{operationId}}: unsupported response type"));
}
return localVarResult;
{{/returnType}}
{{/vendorExtensions.x-codegen-response-ishttpcontent}}
});
}
{{/operation}}
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}
{{/operations}}

View file

@ -0,0 +1,107 @@
{{>licenseInfo}}
/*
* ApiClient.h
*
* This is an API client responsible for stating the HTTP calls
*/
#ifndef {{apiHeaderGuardPrefix}}_ApiClient_H_
#define {{apiHeaderGuardPrefix}}_ApiClient_H_
{{{defaultInclude}}}
#include "{{packageName}}/ApiConfiguration.h"
#include "{{packageName}}/ApiException.h"
#include "{{packageName}}/IHttpBody.h"
#include "{{packageName}}/HttpContent.h"
{{^hasModelImport}}
#include "{{packageName}}/ModelBase.h"
{{/hasModelImport}}
#if defined (_WIN32) || defined (_WIN64)
#undef U
#endif
#include <cpprest/details/basic_types.h>
#include <cpprest/http_client.h>
#include <memory>
#include <vector>
#include <functional>
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
using namespace {{modelNamespace}};
class {{declspec}} ApiClient
{
public:
ApiClient( std::shared_ptr<const ApiConfiguration> configuration = nullptr );
virtual ~ApiClient();
typedef std::function<void(web::http::status_code, const web::http::http_headers&)> ResponseHandlerType;
const ResponseHandlerType& getResponseHandler() const;
void setResponseHandler(const ResponseHandlerType& responseHandler);
std::shared_ptr<const ApiConfiguration> getConfiguration() const;
void setConfiguration(std::shared_ptr<const ApiConfiguration> configuration);
static utility::string_t parameterToString(utility::string_t value);
static utility::string_t parameterToString(int32_t value);
static utility::string_t parameterToString(int64_t value);
static utility::string_t parameterToString(float value);
static utility::string_t parameterToString(double value);
static utility::string_t parameterToString(const utility::datetime &value);
static utility::string_t parameterToString(bool value);
{{^hasModelImport}}
static utility::string_t parameterToString(const ModelBase& value);
{{/hasModelImport}}
template<class T>
static utility::string_t parameterToString(const std::vector<T>& value);
template<class T>
static utility::string_t parameterToString(const std::shared_ptr<T>& value);
pplx::task<web::http::http_response> callApi(
const utility::string_t& path,
const utility::string_t& method,
const std::map<utility::string_t, utility::string_t>& queryParams,
const std::shared_ptr<IHttpBody> postBody,
const std::map<utility::string_t, utility::string_t>& headerParams,
const std::map<utility::string_t, utility::string_t>& formParams,
const std::map<utility::string_t, std::shared_ptr<HttpContent>>& fileParams,
const utility::string_t& contentType
) const;
protected:
ResponseHandlerType m_ResponseHandler;
std::shared_ptr<const ApiConfiguration> m_Configuration;
};
template<class T>
utility::string_t ApiClient::parameterToString(const std::vector<T>& value)
{
utility::stringstream_t ss;
for( size_t i = 0; i < value.size(); i++)
{
if( i > 0) ss << utility::conversions::to_string_t(", ");
ss << ApiClient::parameterToString(value[i]);
}
return ss.str();
}
template<class T>
utility::string_t ApiClient::parameterToString(const std::shared_ptr<T>& value)
{
return parameterToString(*value.get());
}
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}
#endif /* {{apiHeaderGuardPrefix}}_ApiClient_H_ */

View file

@ -0,0 +1,203 @@
{{>licenseInfo}}
#include "{{packageName}}/ApiClient.h"
#include "{{packageName}}/MultipartFormData.h"
#include "{{packageName}}/ModelBase.h"
#include <sstream>
#include <limits>
#include <iomanip>
template <typename T>
utility::string_t toString(const T value)
{
utility::ostringstream_t out;
out << std::setprecision(std::numeric_limits<T>::digits10) << std::fixed << value;
return out.str();
}
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
using namespace {{modelNamespace}};
ApiClient::ApiClient(std::shared_ptr<const ApiConfiguration> configuration )
: m_Configuration(configuration)
{
}
ApiClient::~ApiClient()
{
}
const ApiClient::ResponseHandlerType& ApiClient::getResponseHandler() const {
return m_ResponseHandler;
}
void ApiClient::setResponseHandler(const ResponseHandlerType& responseHandler) {
m_ResponseHandler = responseHandler;
}
std::shared_ptr<const ApiConfiguration> ApiClient::getConfiguration() const
{
return m_Configuration;
}
void ApiClient::setConfiguration(std::shared_ptr<const ApiConfiguration> configuration)
{
m_Configuration = configuration;
}
utility::string_t ApiClient::parameterToString(utility::string_t value)
{
return value;
}
utility::string_t ApiClient::parameterToString(int64_t value)
{
std::stringstream valueAsStringStream;
valueAsStringStream << value;
return utility::conversions::to_string_t(valueAsStringStream.str());
}
utility::string_t ApiClient::parameterToString(int32_t value)
{
std::stringstream valueAsStringStream;
valueAsStringStream << value;
return utility::conversions::to_string_t(valueAsStringStream.str());
}
utility::string_t ApiClient::parameterToString(float value)
{
return utility::conversions::to_string_t(toString(value));
}
utility::string_t ApiClient::parameterToString(double value)
{
return utility::conversions::to_string_t(toString(value));
}
utility::string_t ApiClient::parameterToString(const utility::datetime &value)
{
return utility::conversions::to_string_t(value.to_string(utility::datetime::ISO_8601));
}
{{^hasModelImport}}
utility::string_t ApiClient::parameterToString(const ModelBase& value)
{
return value.toJson().serialize();
}
{{/hasModelImport}}
utility::string_t ApiClient::parameterToString(bool value)
{
std::stringstream valueAsStringStream;
valueAsStringStream << std::boolalpha << value;
return utility::conversions::to_string_t(valueAsStringStream.str());
}
pplx::task<web::http::http_response> ApiClient::callApi(
const utility::string_t& path,
const utility::string_t& method,
const std::map<utility::string_t, utility::string_t>& queryParams,
const std::shared_ptr<IHttpBody> postBody,
const std::map<utility::string_t, utility::string_t>& headerParams,
const std::map<utility::string_t, utility::string_t>& formParams,
const std::map<utility::string_t, std::shared_ptr<HttpContent>>& fileParams,
const utility::string_t& contentType
) const
{
if (postBody != nullptr && formParams.size() != 0)
{
throw ApiException(400, utility::conversions::to_string_t("Cannot have body and form params"));
}
if (postBody != nullptr && fileParams.size() != 0)
{
throw ApiException(400, utility::conversions::to_string_t("Cannot have body and file params"));
}
if (fileParams.size() > 0 && contentType != utility::conversions::to_string_t("multipart/form-data"))
{
throw ApiException(400, utility::conversions::to_string_t("Operations with file parameters must be called with multipart/form-data"));
}
web::http::client::http_client client(m_Configuration->getBaseUrl(), m_Configuration->getHttpConfig());
web::http::http_request request;
for (const auto& kvp : headerParams)
{
request.headers().add(kvp.first, kvp.second);
}
if (fileParams.size() > 0)
{
MultipartFormData uploadData;
for (const auto& kvp : formParams)
{
uploadData.add(ModelBase::toHttpContent(kvp.first, kvp.second));
}
for (const auto& kvp : fileParams)
{
uploadData.add(ModelBase::toHttpContent(kvp.first, kvp.second));
}
std::stringstream data;
uploadData.writeTo(data);
auto bodyString = data.str();
const auto length = bodyString.size();
request.set_body(concurrency::streams::bytestream::open_istream(std::move(bodyString)), length, utility::conversions::to_string_t("multipart/form-data; boundary=") + uploadData.getBoundary());
}
else
{
if (postBody != nullptr)
{
std::stringstream data;
postBody->writeTo(data);
auto bodyString = data.str();
const auto length = bodyString.size();
request.set_body(concurrency::streams::bytestream::open_istream(std::move(bodyString)), length, contentType);
}
else
{
if (contentType == utility::conversions::to_string_t("application/json"))
{
web::json::value body_data = web::json::value::object();
for (auto& kvp : formParams)
{
body_data[kvp.first] = ModelBase::toJson(kvp.second);
}
if (!formParams.empty())
{
request.set_body(body_data);
}
}
else
{
web::http::uri_builder formData;
for (const auto& kvp : formParams)
{
formData.append_query(kvp.first, kvp.second);
}
if (!formParams.empty())
{
request.set_body(formData.query(), utility::conversions::to_string_t("application/x-www-form-urlencoded"));
}
}
}
}
web::http::uri_builder builder(path);
for (const auto& kvp : queryParams)
{
builder.append_query(kvp.first, kvp.second);
}
request.set_request_uri(builder.to_uri());
request.set_method(method);
if ( !request.headers().has( web::http::header_names::user_agent ) )
{
request.headers().add( web::http::header_names::user_agent, m_Configuration->getUserAgent() );
}
return client.request(request);
}
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}

View file

@ -0,0 +1,54 @@
{{>licenseInfo}}
/*
* ApiConfiguration.h
*
* This class represents a single item of a multipart-formdata request.
*/
#ifndef {{apiHeaderGuardPrefix}}_ApiConfiguration_H_
#define {{apiHeaderGuardPrefix}}_ApiConfiguration_H_
{{{defaultInclude}}}
#include <cpprest/details/basic_types.h>
#include <cpprest/http_client.h>
#include <map>
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
class {{declspec}} ApiConfiguration
{
public:
ApiConfiguration();
virtual ~ApiConfiguration();
const web::http::client::http_client_config& getHttpConfig() const;
void setHttpConfig( web::http::client::http_client_config& value );
utility::string_t getBaseUrl() const;
void setBaseUrl( const utility::string_t value );
utility::string_t getUserAgent() const;
void setUserAgent( const utility::string_t value );
std::map<utility::string_t, utility::string_t>& getDefaultHeaders();
const std::map<utility::string_t, utility::string_t>& getDefaultHeaders() const;
utility::string_t getApiKey( const utility::string_t& prefix) const;
void setApiKey( const utility::string_t& prefix, const utility::string_t& apiKey );
protected:
utility::string_t m_BaseUrl;
std::map<utility::string_t, utility::string_t> m_DefaultHeaders;
std::map<utility::string_t, utility::string_t> m_ApiKeys;
web::http::client::http_client_config m_HttpConfig;
utility::string_t m_UserAgent;
};
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}
#endif /* {{apiHeaderGuardPrefix}}_ApiConfiguration_H_ */

View file

@ -0,0 +1,73 @@
{{>licenseInfo}}
#include "{{packageName}}/ApiConfiguration.h"
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
ApiConfiguration::ApiConfiguration()
{
}
ApiConfiguration::~ApiConfiguration()
{
}
const web::http::client::http_client_config& ApiConfiguration::getHttpConfig() const
{
return m_HttpConfig;
}
void ApiConfiguration::setHttpConfig( web::http::client::http_client_config& value )
{
m_HttpConfig = value;
}
utility::string_t ApiConfiguration::getBaseUrl() const
{
return m_BaseUrl;
}
void ApiConfiguration::setBaseUrl( const utility::string_t value )
{
m_BaseUrl = value;
}
utility::string_t ApiConfiguration::getUserAgent() const
{
return m_UserAgent;
}
void ApiConfiguration::setUserAgent( const utility::string_t value )
{
m_UserAgent = value;
}
std::map<utility::string_t, utility::string_t>& ApiConfiguration::getDefaultHeaders()
{
return m_DefaultHeaders;
}
const std::map<utility::string_t, utility::string_t>& ApiConfiguration::getDefaultHeaders() const
{
return m_DefaultHeaders;
}
utility::string_t ApiConfiguration::getApiKey( const utility::string_t& prefix) const
{
auto result = m_ApiKeys.find(prefix);
if( result != m_ApiKeys.end() )
{
return result->second;
}
return utility::conversions::to_string_t("");
}
void ApiConfiguration::setApiKey( const utility::string_t& prefix, const utility::string_t& apiKey )
{
m_ApiKeys[prefix] = apiKey;
}
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}

View file

@ -0,0 +1,48 @@
{{>licenseInfo}}
/*
* ApiException.h
*
* This is the exception being thrown in case the api call was not successful
*/
#ifndef {{apiHeaderGuardPrefix}}_ApiException_H_
#define {{apiHeaderGuardPrefix}}_ApiException_H_
{{{defaultInclude}}}
#include <cpprest/details/basic_types.h>
#include <cpprest/http_msg.h>
#include <memory>
#include <map>
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
class {{declspec}} ApiException
: public web::http::http_exception
{
public:
ApiException( int errorCode
, const utility::string_t& message
, std::shared_ptr<std::istream> content = nullptr );
ApiException( int errorCode
, const utility::string_t& message
, std::map<utility::string_t, utility::string_t>& headers
, std::shared_ptr<std::istream> content = nullptr );
virtual ~ApiException();
std::map<utility::string_t, utility::string_t>& getHeaders();
std::shared_ptr<std::istream> getContent() const;
protected:
std::shared_ptr<std::istream> m_Content;
std::map<utility::string_t, utility::string_t> m_Headers;
};
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}
#endif /* {{apiHeaderGuardPrefix}}_ApiBase_H_ */

View file

@ -0,0 +1,41 @@
{{>licenseInfo}}
#include "{{packageName}}/ApiException.h"
{{#apiNamespaceDeclarations}}
namespace {{this}} {
{{/apiNamespaceDeclarations}}
ApiException::ApiException( int errorCode
, const utility::string_t& message
, std::shared_ptr<std::istream> content /*= nullptr*/ )
: web::http::http_exception( errorCode, message )
, m_Content(content)
{
}
ApiException::ApiException( int errorCode
, const utility::string_t& message
, std::map<utility::string_t, utility::string_t>& headers
, std::shared_ptr<std::istream> content /*= nullptr*/ )
: web::http::http_exception( errorCode, message )
, m_Content(content)
, m_Headers(headers)
{
}
ApiException::~ApiException()
{
}
std::shared_ptr<std::istream> ApiException::getContent() const
{
return m_Content;
}
std::map<utility::string_t, utility::string_t>& ApiException::getHeaders()
{
return m_Headers;
}
{{#apiNamespaceDeclarations}}
}
{{/apiNamespaceDeclarations}}

View file

@ -0,0 +1,5 @@
@PACKAGE_INIT@
include(${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake)
check_required_components("@PROJECT_NAME@")

View file

@ -0,0 +1,91 @@
#
# {{{appName}}}
# {{{appDescription}}}
#
# The version of the OpenAPI document: 1.0.0
#
# https://openapi-generator.tech
#
# NOTE: Auto generated by OpenAPI Generator (https://openapi-generator.tech).
cmake_minimum_required (VERSION 3.10)
project({{{packageName}}} CXX)
# Force -fPIC even if the project is configured for building a static library.
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_CXX_STANDARD)
if(DEFINED CMAKE_CXX20_STANDARD_COMPILE_OPTION OR
DEFINED CMAKE_CXX20_EXTENSION_COMPILE_OPTION)
set(CMAKE_CXX_STANDARD 20)
elseif(DEFINED CMAKE_CXX17_STANDARD_COMPILE_OPTION OR
DEFINED CMAKE_CXX17_EXTENSION_COMPILE_OPTION)
set(CMAKE_CXX_STANDARD 17)
elseif(DEFINED CMAKE_CXX14_STANDARD_COMPILE_OPTION OR
DEFINED CMAKE_CXX14_EXTENSION_COMPILE_OPTION)
set(CMAKE_CXX_STANDARD 14)
else()
set(CMAKE_CXX_STANDARD 11)
endif()
endif()
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
find_package(cpprestsdk REQUIRED)
target_compile_definitions(cpprestsdk::cpprest INTERFACE _TURN_OFF_PLATFORM_STRING)
find_package(Boost REQUIRED)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
file(GLOB_RECURSE HEADER_FILES "include/*.h")
file(GLOB_RECURSE SOURCE_FILES "src/*.cpp")
add_library(${PROJECT_NAME} ${HEADER_FILES} ${SOURCE_FILES})
target_compile_options(${PROJECT_NAME}
PRIVATE
$<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>:
-Wall -Wno-unused-variable -Wno-unused-lambda-capture>
)
target_include_directories(${PROJECT_NAME}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_link_libraries(${PROJECT_NAME} PUBLIC Boost::headers cpprestsdk::cpprest)
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}"
)
install(
TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}Targets
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
)
install(
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/${PROJECT_NAME}
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(
FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)
install(
EXPORT ${PROJECT_NAME}Targets
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

View file

@ -0,0 +1,57 @@
#!/bin/sh
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
#
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
git_user_id=$1
git_repo_id=$2
release_note=$3
git_host=$4
if [ "$git_host" = "" ]; then
git_host="{{{gitHost}}}"
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
fi
if [ "$git_user_id" = "" ]; then
git_user_id="{{{gitUserId}}}"
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
fi
if [ "$git_repo_id" = "" ]; then
git_repo_id="{{{gitRepoId}}}"
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
fi
if [ "$release_note" = "" ]; then
release_note="{{{releaseNote}}}"
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
fi
# Initialize the local directory as a Git repository
git init
# Adds the files in the local repository and stages them for commit.
git add .
# Commits the tracked changes and prepares them to be pushed to a remote repository.
git commit -m "$release_note"
# Sets the new remote
git_remote=$(git remote)
if [ "$git_remote" = "" ]; then # git remote not defined
if [ "$GIT_TOKEN" = "" ]; then
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
else
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
fi
fi
git pull origin master
# Pushes (Forces) the changes in the local repository up to the remote repository
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
git push origin master 2>&1 | grep -v 'To https'

View file

@ -0,0 +1,29 @@
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app

View file

@ -0,0 +1,57 @@
{{>licenseInfo}}
/*
* HttpContent.h
*
* This class represents a single item of a multipart-formdata request.
*/
#ifndef {{modelHeaderGuardPrefix}}_HttpContent_H_
#define {{modelHeaderGuardPrefix}}_HttpContent_H_
{{{defaultInclude}}}
#include <cpprest/details/basic_types.h>
#include <memory>
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
class {{declspec}} HttpContent
{
public:
HttpContent();
virtual ~HttpContent();
virtual utility::string_t getContentDisposition() const;
virtual void setContentDisposition( const utility::string_t& value );
virtual utility::string_t getName() const;
virtual void setName( const utility::string_t& value );
virtual utility::string_t getFileName() const;
virtual void setFileName( const utility::string_t& value );
virtual utility::string_t getContentType() const;
virtual void setContentType( const utility::string_t& value );
virtual std::shared_ptr<std::istream> getData() const;
virtual void setData( std::shared_ptr<std::istream> value );
virtual void writeTo( std::ostream& stream );
protected:
// NOTE: no utility::string_t here because those strings can only contain ascii
utility::string_t m_ContentDisposition;
utility::string_t m_Name;
utility::string_t m_FileName;
utility::string_t m_ContentType;
std::shared_ptr<std::istream> m_Data;
};
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}
#endif /* {{modelHeaderGuardPrefix}}_HttpContent_H_ */

View file

@ -0,0 +1,74 @@
{{>licenseInfo}}
#include "{{packageName}}/HttpContent.h"
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
HttpContent::HttpContent()
{
}
HttpContent::~HttpContent()
{
}
utility::string_t HttpContent::getContentDisposition() const
{
return m_ContentDisposition;
}
void HttpContent::setContentDisposition( const utility::string_t & value )
{
m_ContentDisposition = value;
}
utility::string_t HttpContent::getName() const
{
return m_Name;
}
void HttpContent::setName( const utility::string_t & value )
{
m_Name = value;
}
utility::string_t HttpContent::getFileName() const
{
return m_FileName;
}
void HttpContent::setFileName( const utility::string_t & value )
{
m_FileName = value;
}
utility::string_t HttpContent::getContentType() const
{
return m_ContentType;
}
void HttpContent::setContentType( const utility::string_t & value )
{
m_ContentType = value;
}
std::shared_ptr<std::istream> HttpContent::getData() const
{
return m_Data;
}
void HttpContent::setData( std::shared_ptr<std::istream> value )
{
m_Data = value;
}
void HttpContent::writeTo( std::ostream& stream )
{
m_Data->seekg( 0, m_Data->beg );
stream << m_Data->rdbuf();
}
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}

View file

@ -0,0 +1,30 @@
{{>licenseInfo}}
/*
* IHttpBody.h
*
* This is the interface for contents that can be sent to a remote HTTP server.
*/
#ifndef {{modelHeaderGuardPrefix}}_IHttpBody_H_
#define {{modelHeaderGuardPrefix}}_IHttpBody_H_
{{{defaultInclude}}}
#include <iostream>
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
class {{declspec}} IHttpBody
{
public:
virtual ~IHttpBody() { }
virtual void writeTo( std::ostream& stream ) = 0;
};
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}
#endif /* {{modelHeaderGuardPrefix}}_IHttpBody_H_ */

View file

@ -0,0 +1,37 @@
{{>licenseInfo}}
/*
* JsonBody.h
*
* This is a JSON http body which can be submitted via http
*/
#ifndef {{modelHeaderGuardPrefix}}_JsonBody_H_
#define {{modelHeaderGuardPrefix}}_JsonBody_H_
{{{defaultInclude}}}
#include "{{packageName}}/IHttpBody.h"
#include <cpprest/json.h>
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
class {{declspec}} JsonBody
: public IHttpBody
{
public:
JsonBody( const web::json::value& value );
virtual ~JsonBody();
void writeTo( std::ostream& target ) override;
protected:
web::json::value m_Json;
};
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}
#endif /* {{modelHeaderGuardPrefix}}_JsonBody_H_ */

View file

@ -0,0 +1,24 @@
{{>licenseInfo}}
#include "{{packageName}}/JsonBody.h"
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
JsonBody::JsonBody( const web::json::value& json)
: m_Json(json)
{
}
JsonBody::~JsonBody()
{
}
void JsonBody::writeTo( std::ostream& target )
{
m_Json.serialize(target);
}
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}

View file

@ -0,0 +1,15 @@
/**
* {{{appName}}}
* {{{appDescription}}}
*
{{#version}}
* The version of the OpenAPI document: {{{.}}}
{{/version}}
{{#infoEmail}}
* Contact: {{{.}}}
{{/infoEmail}}
*
* NOTE: This class is auto generated by OpenAPI-Generator {{{generatorVersion}}}.
* https://openapi-generator.tech
* Do not edit the class manually.
*/

View file

@ -0,0 +1,288 @@
{{>licenseInfo}}
{{#models}}{{#model}}/*
* {{classname}}.h
*
* {{description}}
*/
#ifndef {{modelHeaderGuardPrefix}}_{{classname}}_H_
#define {{modelHeaderGuardPrefix}}_{{classname}}_H_
{{#hasEnums}}
#include <stdexcept>
{{/hasEnums}}
{{#oneOf}}
{{#-first}}
#include <variant>
{{/-first}}
{{/oneOf}}
{{^parent}}
{{{defaultInclude}}}
#include "{{packageName}}/ModelBase.h"
{{/parent}}
{{#imports}}{{{this}}}
{{/imports}}
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
{{#vendorExtensions.x-has-forward-declarations}}
{{#vendorExtensions.x-forward-declarations}}{{.}}
{{/vendorExtensions.x-forward-declarations}}
{{/vendorExtensions.x-has-forward-declarations}}
{{#oneOf}}{{#-first}}
class {{declspec}} {{classname}}
{
public:
{{classname}}() = default;
~{{classname}}() = default;
/////////////////////////////////////////////
void validate();
web::json::value toJson() const;
template<typename Target>
bool fromJson(const web::json::value& json) {
// convert json to Target type
Target target;
if (!target.fromJson(json)) {
return false;
}
m_variantValue = target;
return true;
}
void toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) const;
template<typename Target>
bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) {
// convert multipart to Target type
Target target;
if (!target.fromMultiPart(multipart, namePrefix)) {
return false;
}
m_variantValue = target;
return true;
}
/////////////////////////////////////////////
/// {{classname}} members
using VariantType = std::variant<{{#oneOf}}{{^-first}}, {{/-first}}{{{.}}}{{/oneOf}}>;
const VariantType& getVariant() const;
void setVariant(VariantType value);
protected:
VariantType m_variantValue;
};
{{/-first}}{{/oneOf}}
{{^oneOf}}
{{#isEnum}}
class {{declspec}} {{classname}}
: public {{{parent}}}{{^parent}}ModelBase{{/parent}}
{
public:
{{classname}}();
{{classname}}(utility::string_t str);
operator utility::string_t() const {
return enumToStrMap.at(getValue());
}
{{! operator std::string() const {
return enumToStrMap.at(getValue());
} }}
virtual ~{{classname}}();
/////////////////////////////////////////////
/// ModelBase overrides
void validate() override;
web::json::value toJson() const override;
bool fromJson(const web::json::value& json) override;
void toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) const override;
bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) override;
enum class e{{classname}}
{
{{#allowableValues}}
{{#enumVars}}
{{#enumDescription}}
/// <summary>
/// {{.}}
/// </summary>
{{/enumDescription}}
{{{name}}}{{^last}},{{/last}}
{{/enumVars}}
{{/allowableValues}}
};
e{{classname}} getValue() const;
void setValue(e{{classname}} const value);
protected:
e{{classname}} m_value;
std::map<e{{classname}},utility::string_t> enumToStrMap = {
{{#allowableValues}}
{{#enumVars}}
{ e{{classname}}::{{{name}}}, _XPLATSTR("{{{name}}}") }{{^-last}},{{/-last}}
{{/enumVars}}
{{/allowableValues}}
};
std::map<utility::string_t,e{{classname}}> strToEnumMap = {
{{#allowableValues}}
{{#enumVars}}
{ _XPLATSTR("{{{name}}}"), e{{classname}}::{{{name}}} }{{^-last}},{{/-last}}
{{/enumVars}}
{{/allowableValues}}
};
};
{{/isEnum}}
{{^isEnum}}
{{#description}}
/// <summary>
/// {{description}}
/// </summary>
{{/description}}
class {{declspec}} {{classname}}
: public {{{parent}}}{{^parent}}ModelBase{{/parent}}
{
public:
{{classname}}();
virtual ~{{classname}}();
/////////////////////////////////////////////
/// ModelBase overrides
void validate() override;
web::json::value toJson() const override;
bool fromJson(const web::json::value& json) override;
void toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) const override;
bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) override;
/////////////////////////////////////////////
/// {{classname}} members
{{! ENUM DEFINITIONS }}
{{#vars}}
{{^isInherited}}
{{#isEnum}}
enum class {{#isContainer}}{{{enumName}}}{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}
{
{{#allowableValues}}
{{#enumVars}}
{{{name}}}{{^last}},{{/last}}
{{/enumVars}}
{{/allowableValues}}
};
{{#description}}
/// <summary>
/// {{description}}
/// </summary>
{{/description}}
{{/isEnum}}
{{/isInherited}}
{{/vars}}
{{#vars}}
{{^isInherited}}
{{#isEnum}}
{{#isContainer}}
{{! ENUM CONVERSIONS }}
{{{enumName}}} to{{{enumName}}}(const utility::string_t& value) const;
const utility::string_t from{{{enumName}}}(const {{{enumName}}} value) const;
{{#isArray}}
{{{datatypeWithEnum}}} to{{{enumName}}}(const {{{dataType}}}& value) const;
{{{dataType}}} from{{{enumName}}}(const {{{datatypeWithEnum}}}& value) const;
{{/isArray}}{{/isContainer}}{{^isContainer}}
{{{datatypeWithEnum}}} to{{{datatypeWithEnum}}}(const utility::string_t& value) const;
const utility::string_t from{{{datatypeWithEnum}}}(const {{{datatypeWithEnum}}} value) const;
{{/isContainer}}
{{/isEnum}}
{{/isInherited}}
{{/vars}}
{{! SETTER AND GETTERS }}
{{#vars}}
{{^isInherited}}
{{#description}}
/// <summary>
/// {{description}}
/// </summary>
{{/description}}
{{#isContainer}}
{{^isEnum}}
{{{dataType}}} {{getter}}() const;
{{/isEnum}}
{{/isContainer}}
{{^isContainer}}
{{^isEnum}}
{{{dataType}}} {{getter}}() const;
{{/isEnum}}
{{/isContainer}}
{{#isEnum}}
{{^isMap}}
{{{datatypeWithEnum}}} {{getter}}() const;
{{/isMap}}
{{#isMap}}
{{{dataType}}} {{getter}}() const;
{{/isMap}}
{{/isEnum}}
bool {{nameInCamelCase}}IsSet() const;
void unset{{name}}();
{{#isPrimitiveType}}
void {{setter}}({{{dataType}}} value);
{{/isPrimitiveType}}
{{^isPrimitiveType}}
{{^isEnum}}
void {{setter}}(const {{{dataType}}}& value);
{{/isEnum}}
{{/isPrimitiveType}}
{{#isEnum}}
void {{setter}}(const {{^isMap}}{{{datatypeWithEnum}}}{{/isMap}}{{#isMap}}{{{dataType}}}{{/isMap}} value);
{{/isEnum}}
{{/isInherited}}
{{/vars}}
protected:
{{#vars}}
{{^isInherited}}
{{^isEnum}}
{{{dataType}}} m_{{name}};
{{/isEnum}}
{{#isEnum}}
{{^isMap}}{{{datatypeWithEnum}}}{{/isMap}}{{#isMap}}{{{dataType}}}{{/isMap}} m_{{name}};
{{/isEnum}}
bool m_{{name}}IsSet;
{{/isInherited}}
{{/vars}}
};
{{/isEnum}}
{{/oneOf}}
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}
#endif /* {{modelHeaderGuardPrefix}}_{{classname}}_H_ */
{{/model}}
{{/models}}

View file

@ -0,0 +1,457 @@
{{>licenseInfo}}
{{#models}}{{#model}}
#include "{{packageName}}/model/{{classFilename}}.h"
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
{{#oneOf}}
{{#-first}}
void {{classname}}::validate()
{
// TODO: implement validation
}
const {{classname}}::VariantType& {{classname}}::getVariant() const
{
return m_variantValue;
}
void {{classname}}::setVariant({{classname}}::VariantType value)
{
m_variantValue = value;
}
web::json::value {{classname}}::toJson() const
{
web::json::value val = web::json::value::object();
std::visit([&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::monostate>) {
val = web::json::value::null();
} else {
val = arg.toJson();
}
}, m_variantValue);
return val;
}
void {{classname}}::toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) const
{
std::visit([&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (!std::is_same_v<T, std::monostate>) {
arg.toMultipart(multipart, prefix);
}
}, m_variantValue);
}
{{#oneOf}}
template bool {{classname}}::fromJson<{{.}}>(const web::json::value& json);
template bool {{classname}}::fromMultiPart<{{.}}>(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix);
{{/oneOf}}
{{/-first}}
{{/oneOf}}
{{^oneOf}}
{{#isEnum}}
namespace
{
using EnumUnderlyingType = {{#isNumeric}}int64_t{{/isNumeric}}{{^isNumeric}}utility::string_t{{/isNumeric}};
{{classname}}::e{{classname}} toEnum(const EnumUnderlyingType& val)
{
{{#allowableValues}}
{{#isNumeric}}
switch (val)
{
{{#enumVars}}
case {{value}}:
return {{classname}}::e{{classname}}::{{name}};
{{#-last}}
default:
break;
{{/-last}}
{{/enumVars}}
}
{{/isNumeric}}
{{^isNumeric}}
{{#enumVars}}
if (val == utility::conversions::to_string_t(_XPLATSTR("{{{value}}}")))
return {{classname}}::e{{classname}}::{{name}};
{{/enumVars}}
{{/isNumeric}}
{{/allowableValues}}
return {};
}
EnumUnderlyingType fromEnum({{classname}}::e{{classname}} e)
{
{{#allowableValues}}
switch (e)
{
{{#enumVars}}
case {{classname}}::e{{classname}}::{{name}}:
return {{#isNumeric}}{{value}}{{/isNumeric}}{{^isNumeric}}_XPLATSTR("{{value}}"){{/isNumeric}};
{{#-last}}
default:
break;
{{/-last}}
{{/enumVars}}
}
{{/allowableValues}}
return {};
}
}
{{classname}}::{{classname}}()
{
}
{{classname}}::~{{classname}}()
{
}
void {{classname}}::validate()
{
// TODO: implement validation
}
web::json::value {{classname}}::toJson() const
{
auto val = fromEnum(m_value);
return web::json::value(val);
}
bool {{classname}}::fromJson(const web::json::value& val)
{
m_value = toEnum({{#isNumeric}}val.as_number().to_int64(){{/isNumeric}}{{^isNumeric}}val.as_string(){{/isNumeric}});
return true;
}
void {{classname}}::toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) const
{
utility::string_t namePrefix = prefix;
if (!namePrefix.empty() && namePrefix.back() != _XPLATSTR('.'))
{
namePrefix.push_back(_XPLATSTR('.'));
}
auto e = fromEnum(m_value);
multipart->add(ModelBase::toHttpContent(namePrefix, e));
}
bool {{classname}}::fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix)
{
bool ok = true;
utility::string_t namePrefix = prefix;
if (!namePrefix.empty() && namePrefix.back() != _XPLATSTR('.'))
{
namePrefix.push_back(_XPLATSTR('.'));
}
{
EnumUnderlyingType e;
ok = ModelBase::fromHttpContent(multipart->getContent(namePrefix), e);
if (ok)
{
auto v = toEnum(e);
setValue(v);
}
}
return ok;
}
{{classname}}::e{{classname}} {{classname}}::getValue() const
{
return m_value;
}
void {{classname}}::setValue({{classname}}::e{{classname}} const value)
{
m_value = value;
}
{{classname}}::{{classname}}(utility::string_t str){
setValue( strToEnumMap[str] );
}
{{/isEnum}}
{{^isEnum}}
{{classname}}::{{classname}}()
{
{{#vars}}
{{^isInherited}}
{{^isContainer}}
{{^isEnum}}
{{#isPrimitiveType}}
m_{{name}} = {{{defaultValue}}};
{{/isPrimitiveType}}
{{^isPrimitiveType}}
{{#isString}}
m_{{name}} = {{{defaultValue}}};
{{/isString}}
{{#isDateTime}}
m_{{name}} = {{{defaultValue}}};
{{/isDateTime}}
{{/isPrimitiveType}}
{{/isEnum}}
{{/isContainer}}
m_{{name}}IsSet = false;
{{/isInherited}}
{{/vars}}
}
{{classname}}::~{{classname}}()
{
}
void {{classname}}::validate()
{
// TODO: implement validation
}
web::json::value {{classname}}::toJson() const
{
{{#parent}}
web::json::value val = this->{{{.}}}::toJson();
{{/parent}}
{{^parent}}
web::json::value val = web::json::value::object();
{{/parent}}
{{#vars}}
{{^isInherited}}
if(m_{{name}}IsSet)
{
{{#isEnum}}{{#isContainer}}{{#isArray}}
{{{dataType}}} refVal = from{{{enumName}}}(m_{{name}});
{{/isArray}}{{#isMap}}
val[utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))] = ModelBase::toJson(m_{{name}});
{{/isMap}}{{/isContainer}}{{^isContainer}}
utility::string_t refVal = from{{{datatypeWithEnum}}}(m_{{name}});
{{/isContainer}}{{^isMap}}val[utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))] = ModelBase::toJson(refVal);
{{/isMap}}{{/isEnum}}
{{^isEnum}}
val[utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))] = ModelBase::toJson(m_{{name}});
{{/isEnum}}
}
{{/isInherited}}
{{/vars}}
return val;
}
bool {{classname}}::fromJson(const web::json::value& val)
{
bool ok = true;
{{#parent}}
ok &= this->{{{.}}}::fromJson(val);
{{/parent}}
{{#vars}}
{{^isInherited}}
if(val.has_field(utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))))
{
const web::json::value& fieldValue = val.at(utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")));
if(!fieldValue.is_null())
{
{{{dataType}}} refVal_{{setter}};
ok &= ModelBase::fromJson(fieldValue, refVal_{{setter}});
{{^isEnum}}
{{setter}}(refVal_{{setter}});
{{/isEnum}}
{{#isEnum}}{{#isContainer}}{{#isArray}}
{{setter}}(to{{{enumName}}}(refVal_{{setter}}));
{{/isArray}}{{#isMap}}
{{setter}}(refVal_{{setter}});
{{/isMap}}{{/isContainer}}{{^isContainer}}
{{setter}}(to{{{datatypeWithEnum}}}(refVal_{{setter}}));
{{/isContainer}}{{/isEnum}}
}
}
{{/isInherited}}
{{/vars}}
return ok;
}
void {{classname}}::toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) const
{
utility::string_t namePrefix = prefix;
if(namePrefix.size() > 0 && namePrefix.substr(namePrefix.size() - 1) != utility::conversions::to_string_t(_XPLATSTR(".")))
{
namePrefix += utility::conversions::to_string_t(_XPLATSTR("."));
}
{{#vars}}
if(m_{{name}}IsSet)
{
{{^isEnum}}
multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")), m_{{name}}));
{{/isEnum}}
{{#isEnum}}
{{#isContainer}}
{{#isArray}}
multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")), from{{{enumName}}}(m_{{name}})));
{{/isArray}}{{#isMap}}
multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")), m_{{name}}));
{{/isMap}}
{{/isContainer}}
{{^isContainer}}
multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")), from{{{datatypeWithEnum}}}(m_{{name}})));
{{/isContainer}}
{{/isEnum}}
}
{{/vars}}
}
bool {{classname}}::fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix)
{
bool ok = true;
utility::string_t namePrefix = prefix;
if(namePrefix.size() > 0 && namePrefix.substr(namePrefix.size() - 1) != utility::conversions::to_string_t(_XPLATSTR(".")))
{
namePrefix += utility::conversions::to_string_t(_XPLATSTR("."));
}
{{#vars}}
if(multipart->hasContent(utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))))
{
{{{dataType}}} refVal_{{setter}};
ok &= ModelBase::fromHttpContent(multipart->getContent(utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))), refVal_{{setter}} );
{{^isEnum}}
{{setter}}(refVal_{{setter}});
{{/isEnum}}
{{#isEnum}}
{{#isContainer}}
{{#isArray}}
{{setter}}(to{{{enumName}}}(refVal_{{setter}}));
{{/isArray}}
{{#isMap}}
{{setter}}(refVal_{{setter}});
{{/isMap}}
{{/isContainer}}
{{^isContainer}}
{{setter}}(to{{{datatypeWithEnum}}}(refVal_{{setter}}));
{{/isContainer}}
{{/isEnum}}
}
{{/vars}}
return ok;
}
{{#vars}}
{{^isInherited}}
{{#isEnum}}
{{#isContainer}}
{{classname}}::{{{enumName}}} {{classname}}::to{{{enumName}}}(const utility::string_t& value) const
{{/isContainer}}
{{^isContainer}}
{{classname}}::{{{datatypeWithEnum}}} {{classname}}::to{{{datatypeWithEnum}}}(const {{dataType}}& value) const
{{/isContainer}}
{
{{#allowableValues}}{{#enumVars}}
if (value == utility::conversions::to_string_t("{{value}}")) {
return {{#isContainer}}{{{enumName}}}{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}::{{name}};
}
{{/enumVars}}{{/allowableValues}}
throw std::invalid_argument("Invalid value for conversion to {{{datatypeWithEnum}}}");
}
{{#isContainer}}
const utility::string_t {{classname}}::from{{{enumName}}}(const {{{enumName}}} value) const
{{/isContainer}}{{^isContainer}}
const {{dataType}} {{classname}}::from{{{datatypeWithEnum}}}(const {{{datatypeWithEnum}}} value) const
{{/isContainer}}
{
switch(value)
{
{{#allowableValues}}{{#enumVars}}
case {{#isContainer}}{{{enumName}}}{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}::{{name}}: return utility::conversions::to_string_t("{{value}}");
{{/enumVars}}{{/allowableValues}}
}
}
{{#isContainer}}
{{#isArray}}
{{{dataType}}} {{{classname}}}::from{{{enumName}}}(const {{{datatypeWithEnum}}}& value) const
{
{{{dataType}}} ret;
for (auto it = value.begin(); it != value.end(); it++) {
ret.push_back(from{{{enumName}}}(*it));
}
return ret;
}
{{{baseType}}}<{{classname}}::{{{enumName}}}> {{{classname}}}::to{{{enumName}}}(const {{{dataType}}}& value) const
{
{{{datatypeWithEnum}}} ret;
for (auto it = value.begin(); it != value.end(); it++) {
ret.push_back(to{{{enumName}}}(*it));
}
return ret;
}
{{/isArray}}
{{/isContainer}}
{{/isEnum}}
{{/isInherited}}
{{/vars}}
{{#vars}}
{{^isInherited}}
{{#isContainer}}
{{^isEnum}}
{{{dataType}}} {{classname}}::{{getter}}() const
{
return m_{{name}};
}
{{/isEnum}}
{{/isContainer}}
{{^isContainer}}
{{^isEnum}}
{{{dataType}}} {{classname}}::{{getter}}() const
{
return m_{{name}};
}
{{/isEnum}}
{{/isContainer}}
{{#isEnum}}
{{^isMap}}{{#isArray}}{{{baseType}}}<{{/isArray}}{{{classname}}}::{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isMap}}{{#isMap}}{{{dataType}}}{{/isMap}} {{classname}}::{{getter}}() const
{
return m_{{name}};
}
{{/isEnum}}
{{#isPrimitiveType}}
void {{classname}}::{{setter}}({{{dataType}}} value)
{{/isPrimitiveType}}{{^isPrimitiveType}}{{^isEnum}}
void {{classname}}::{{setter}}(const {{{dataType}}}& value)
{{/isEnum}}{{/isPrimitiveType}}{{#isEnum}}
void {{classname}}::{{setter}}(const {{^isMap}}{{{datatypeWithEnum}}}{{/isMap}}{{#isMap}}{{{dataType}}}{{/isMap}} value)
{{/isEnum}}
{
m_{{name}} = value;
m_{{name}}IsSet = true;
}
bool {{classname}}::{{nameInCamelCase}}IsSet() const
{
return m_{{name}}IsSet;
}
void {{classname}}::unset{{name}}()
{
m_{{name}}IsSet = false;
}
{{/isInherited}}{{/vars}}
{{/isEnum}}
{{/oneOf}}
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}
{{/model}}
{{/models}}

View file

@ -0,0 +1,493 @@
{{>licenseInfo}}
/*
* ModelBase.h
*
* This is the base class for all model classes
*/
#ifndef {{modelHeaderGuardPrefix}}_ModelBase_H_
#define {{modelHeaderGuardPrefix}}_ModelBase_H_
{{{defaultInclude}}}
#include "{{packageName}}/HttpContent.h"
#include "{{packageName}}/MultipartFormData.h"
#include <cpprest/details/basic_types.h>
#include <cpprest/json.h>
#include <map>
#include <set>
#include <vector>
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
class {{declspec}} ModelBase
{
public:
ModelBase();
virtual ~ModelBase();
virtual void validate() = 0;
virtual web::json::value toJson() const = 0;
virtual bool fromJson( const web::json::value& json ) = 0;
virtual void toMultipart( std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix ) const = 0;
virtual bool fromMultiPart( std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix ) = 0;
virtual bool isSet() const;
static utility::string_t toString( const bool val );
static utility::string_t toString( const float val );
static utility::string_t toString( const double val );
static utility::string_t toString( const int32_t val );
static utility::string_t toString( const int64_t val );
static utility::string_t toString( const utility::string_t &val );
static utility::string_t toString( const utility::datetime &val );
static utility::string_t toString( const web::json::value &val );
static utility::string_t toString( const std::shared_ptr<HttpContent>& val );
template <typename T>
static utility::string_t toString( const std::shared_ptr<T>& val );
template <typename T>
static utility::string_t toString( const std::vector<T> & val );
template <typename T>
static utility::string_t toString( const std::set<T> & val );
static web::json::value toJson( bool val );
static web::json::value toJson( float val );
static web::json::value toJson( double val );
static web::json::value toJson( int32_t val );
static web::json::value toJson( int64_t val );
static web::json::value toJson( const utility::string_t& val );
static web::json::value toJson( const utility::datetime& val );
static web::json::value toJson( const web::json::value& val );
static web::json::value toJson( const std::shared_ptr<HttpContent>& val );
template<typename T>
static web::json::value toJson( const std::shared_ptr<T>& val );
static web::json::value toJson( const std::shared_ptr<utility::datetime>& val );
template<typename T>
static web::json::value toJson( const std::vector<T>& val );
template<typename T>
static web::json::value toJson( const std::set<T>& val );
template<typename T>
static web::json::value toJson( const std::map<utility::string_t, T>& val );
static bool fromString( const utility::string_t& val, bool & );
static bool fromString( const utility::string_t& val, float & );
static bool fromString( const utility::string_t& val, double & );
static bool fromString( const utility::string_t& val, int32_t & );
static bool fromString( const utility::string_t& val, int64_t & );
static bool fromString( const utility::string_t& val, utility::string_t & );
static bool fromString( const utility::string_t& val, utility::datetime & );
static bool fromString( const utility::string_t& val, web::json::value & );
static bool fromString( const utility::string_t& val, std::shared_ptr<HttpContent> & );
template<typename T>
static bool fromString( const utility::string_t& val, std::shared_ptr<T>& );
static bool fromString( const utility::string_t& val, std::shared_ptr<utility::datetime>& outVal );
template<typename T>
static bool fromString( const utility::string_t& val, std::vector<T> & );
template<typename T>
static bool fromString( const utility::string_t& val, std::set<T> & );
template<typename T>
static bool fromString( const utility::string_t& val, std::map<utility::string_t, T> & );
static bool fromJson( const web::json::value& val, bool & );
static bool fromJson( const web::json::value& val, float & );
static bool fromJson( const web::json::value& val, double & );
static bool fromJson( const web::json::value& val, int32_t & );
static bool fromJson( const web::json::value& val, int64_t & );
static bool fromJson( const web::json::value& val, utility::string_t & );
static bool fromJson( const web::json::value& val, utility::datetime & );
static bool fromJson( const web::json::value& val, web::json::value & );
static bool fromJson( const web::json::value& val, std::shared_ptr<HttpContent> & );
template<typename T>
static bool fromJson( const web::json::value& val, std::shared_ptr<T>& );
static bool fromJson( const web::json::value& val, std::shared_ptr<utility::datetime> &outVal );
template<typename T>
static bool fromJson( const web::json::value& val, std::vector<T> & );
template<typename T>
static bool fromJson( const web::json::value& val, std::set<T> & );
template<typename T>
static bool fromJson( const web::json::value& val, std::map<utility::string_t, T> & );
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, bool value, const utility::string_t& contentType = utility::conversions::to_string_t("") );
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, float value, const utility::string_t& contentType = utility::conversions::to_string_t("") );
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, double value, const utility::string_t& contentType = utility::conversions::to_string_t("") );
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, int32_t value, const utility::string_t& contentType = utility::conversions::to_string_t("") );
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, int64_t value, const utility::string_t& contentType = utility::conversions::to_string_t("") );
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const utility::string_t& value, const utility::string_t& contentType = utility::conversions::to_string_t(""));
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const utility::datetime& value, const utility::string_t& contentType = utility::conversions::to_string_t(""));
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const web::json::value& value, const utility::string_t& contentType = utility::conversions::to_string_t("application/json") );
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::shared_ptr<HttpContent>& );
template <typename T>
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::shared_ptr<T>& , const utility::string_t& contentType = utility::conversions::to_string_t("application/json") );
static std::shared_ptr<HttpContent> toHttpContent(const utility::string_t& name, const std::shared_ptr<utility::datetime>& value , const utility::string_t& contentType = utility::conversions::to_string_t("application/json") );
template <typename T>
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::vector<T>& value, const utility::string_t& contentType = utility::conversions::to_string_t("") );
template <typename T>
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::set<T>& value, const utility::string_t& contentType = utility::conversions::to_string_t("") );
template <typename T>
static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::map<utility::string_t, T>& value, const utility::string_t& contentType = utility::conversions::to_string_t("") );
static bool fromHttpContent( std::shared_ptr<HttpContent> val, bool & );
static bool fromHttpContent( std::shared_ptr<HttpContent> val, float & );
static bool fromHttpContent( std::shared_ptr<HttpContent> val, double & );
static bool fromHttpContent( std::shared_ptr<HttpContent> val, int64_t & );
static bool fromHttpContent( std::shared_ptr<HttpContent> val, int32_t & );
static bool fromHttpContent( std::shared_ptr<HttpContent> val, utility::string_t & );
static bool fromHttpContent( std::shared_ptr<HttpContent> val, utility::datetime & );
static bool fromHttpContent( std::shared_ptr<HttpContent> val, web::json::value & );
static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::shared_ptr<HttpContent>& );
template <typename T>
static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::shared_ptr<T>& );
template <typename T>
static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::vector<T> & );
template <typename T>
static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::set<T> & );
template <typename T>
static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::map<utility::string_t, T> & );
static utility::string_t toBase64( utility::string_t value );
static utility::string_t toBase64( std::shared_ptr<std::istream> value );
static std::shared_ptr<std::istream> fromBase64( const utility::string_t& encoded );
protected:
bool m_IsSet;
};
template <typename T>
utility::string_t ModelBase::toString( const std::shared_ptr<T>& val )
{
utility::stringstream_t ss;
if( val != nullptr )
{
val->toJson().serialize(ss);
}
return utility::string_t(ss.str());
}
// std::vector to string
template<typename T>
utility::string_t ModelBase::toString( const std::vector<T> & val )
{
utility::string_t strArray;
for ( const auto &item : val )
{
strArray.append( toString(item) + "," );
}
if (val.count() > 0)
{
strArray.pop_back();
}
return strArray;
}
// std::set to string
template<typename T>
utility::string_t ModelBase::toString( const std::set<T> & val )
{
utility::string_t strArray;
for ( const auto &item : val )
{
strArray.append( toString(item) + "," );
}
if (val.count() > 0)
{
strArray.pop_back();
}
return strArray;
}
template<typename T>
web::json::value ModelBase::toJson( const std::shared_ptr<T>& val )
{
web::json::value retVal;
if(val != nullptr)
{
retVal = val->toJson();
}
return retVal;
}
// std::vector to json
template<typename T>
web::json::value ModelBase::toJson( const std::vector<T>& value )
{
std::vector<web::json::value> ret;
for ( const auto& x : value )
{
ret.push_back( toJson(x) );
}
return web::json::value::array(ret);
}
// std::set to json
template<typename T>
web::json::value ModelBase::toJson( const std::set<T>& value )
{
// There's no prototype web::json::value::array(...) taking a std::set parameter. Converting to std::vector to get an array.
std::vector<web::json::value> ret;
for ( const auto& x : value )
{
ret.push_back( toJson(x) );
}
return web::json::value::array(ret);
}
template<typename T>
web::json::value ModelBase::toJson( const std::map<utility::string_t, T>& val )
{
web::json::value obj;
for ( const auto &itemkey : val )
{
obj[itemkey.first] = toJson( itemkey.second );
}
return obj;
}
template<typename T>
bool ModelBase::fromString( const utility::string_t& val, std::shared_ptr<T>& outVal )
{
bool ok = false;
if(outVal == nullptr)
{
outVal = std::make_shared<T>();
}
if( outVal != nullptr )
{
ok = outVal->fromJson(web::json::value::parse(val));
}
return ok;
}
template<typename T>
bool ModelBase::fromString(const utility::string_t& val, std::vector<T>& outVal )
{
bool ok = true;
web::json::value jsonValue = web::json::value::parse(val);
if (jsonValue.is_array())
{
for (const web::json::value& jitem : jsonValue.as_array())
{
T item;
ok &= fromJson(jitem, item);
outVal.push_back(item);
}
}
else
{
T item;
ok = fromJson(jsonValue, item);
outVal.push_back(item);
}
return ok;
}
template<typename T>
bool ModelBase::fromString(const utility::string_t& val, std::set<T>& outVal )
{
bool ok = true;
web::json::value jsonValue = web::json::value::parse(val);
if (jsonValue.is_array())
{
for (const web::json::value& jitem : jsonValue.as_array())
{
T item;
ok &= fromJson(jitem, item);
outVal.insert(item);
}
}
else
{
T item;
ok = fromJson(jsonValue, item);
outVal.insert(item);
}
return ok;
}
template<typename T>
bool ModelBase::fromString(const utility::string_t& val, std::map<utility::string_t, T>& outVal )
{
bool ok = false;
web::json::value jsonValue = web::json::value::parse(val);
if (jsonValue.is_array())
{
for (const web::json::value& jitem : jsonValue.as_array())
{
T item;
ok &= fromJson(jitem, item);
outVal.insert({ val, item });
}
}
else
{
T item;
ok = fromJson(jsonValue, item);
outVal.insert({ val, item });
}
return ok;
}
template<typename T>
bool ModelBase::fromJson( const web::json::value& val, std::shared_ptr<T> &outVal )
{
bool ok = false;
if(outVal == nullptr)
{
outVal = std::make_shared<T>();
}
if( outVal != nullptr )
{
ok = outVal->fromJson(val);
}
return ok;
}
template<typename T>
bool ModelBase::fromJson( const web::json::value& val, std::vector<T> &outVal )
{
bool ok = true;
if (val.is_array())
{
for (const web::json::value & jitem : val.as_array())
{
T item;
ok &= fromJson(jitem, item);
outVal.push_back(item);
}
}
else
{
ok = false;
}
return ok;
}
template<typename T>
bool ModelBase::fromJson(const web::json::value& val, std::set<T>& outVal )
{
bool ok = true;
if (val.is_array())
{
for (const web::json::value& jitem : val.as_array())
{
T item;
ok &= fromJson(jitem, item);
outVal.insert(item);
}
}
else
{
T item;
ok = fromJson(val, item);
outVal.insert(item);
}
return ok;
}
template<typename T>
bool ModelBase::fromJson( const web::json::value& jval, std::map<utility::string_t, T> &outVal )
{
bool ok = true;
if ( jval.is_object() )
{
auto obj = jval.as_object();
for( auto objItr = obj.begin() ; objItr != obj.end() ; objItr++ )
{
T itemVal;
ok &= fromJson(objItr->second, itemVal);
outVal.insert(std::pair<utility::string_t, T>(objItr->first, itemVal));
}
}
else
{
ok = false;
}
return ok;
}
template <typename T>
std::shared_ptr<HttpContent> ModelBase::toHttpContent(const utility::string_t& name, const std::shared_ptr<T>& value , const utility::string_t& contentType )
{
std::shared_ptr<HttpContent> content = std::make_shared<HttpContent>();
if (value != nullptr )
{
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
content->setData( std::make_shared<std::stringstream>( utility::conversions::to_utf8string(value->toJson().serialize()) ) );
}
return content;
}
template <typename T>
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const std::vector<T>& value, const utility::string_t& contentType )
{
web::json::value json_array = ModelBase::toJson(value);
std::shared_ptr<HttpContent> content = std::make_shared<HttpContent>();
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
content->setData( std::make_shared<std::stringstream>( utility::conversions::to_utf8string(json_array.serialize()) ) );
return content;
}
template <typename T>
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const std::set<T>& value, const utility::string_t& contentType )
{
web::json::value json_array = ModelBase::toJson(value);
std::shared_ptr<HttpContent> content = std::make_shared<HttpContent>();
content->setName(name);
content->setContentDisposition(utility::conversions::to_string_t("form-data"));
content->setContentType(contentType);
content->setData( std::make_shared<std::stringstream>( utility::conversions::to_utf8string(json_array.serialize()) ) );
return content;
}
template <typename T>
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const std::map<utility::string_t, T>& value, const utility::string_t& contentType )
{
web::json::value jobj = ModelBase::toJson(value);
std::shared_ptr<HttpContent> content = std::make_shared<HttpContent>();
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
content->setData( std::make_shared<std::stringstream>( utility::conversions::to_utf8string(jobj.serialize()) ) );
return content;
}
template <typename T>
bool ModelBase::fromHttpContent( std::shared_ptr<HttpContent> val, std::shared_ptr<T>& outVal )
{
utility::string_t str;
if(val == nullptr) return false;
if( outVal == nullptr )
{
outVal = std::make_shared<T>();
}
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
template <typename T>
bool ModelBase::fromHttpContent( std::shared_ptr<HttpContent> val, std::vector<T> & outVal )
{
utility::string_t str;
if (val == nullptr) return false;
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
template <typename T>
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, std::set<T>& outVal )
{
utility::string_t str;
if (val == nullptr) return false;
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
template <typename T>
bool ModelBase::fromHttpContent( std::shared_ptr<HttpContent> val, std::map<utility::string_t, T> & outVal )
{
utility::string_t str;
if (val == nullptr) return false;
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}
#endif /* {{modelHeaderGuardPrefix}}_ModelBase_H_ */

View file

@ -0,0 +1,653 @@
{{>licenseInfo}}
#include "{{packageName}}/ModelBase.h"
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
ModelBase::ModelBase(): m_IsSet(false)
{
}
ModelBase::~ModelBase()
{
}
bool ModelBase::isSet() const
{
return m_IsSet;
}
utility::string_t ModelBase::toString( const bool val )
{
utility::stringstream_t ss;
ss << val;
return utility::string_t(ss.str());
}
utility::string_t ModelBase::toString( const float val )
{
utility::stringstream_t ss;
ss << val;
return utility::string_t(ss.str());
}
utility::string_t ModelBase::toString( const double val )
{
utility::stringstream_t ss;
ss << val;
return utility::string_t(ss.str());
}
utility::string_t ModelBase::toString( const int32_t val )
{
utility::stringstream_t ss;
ss << val;
return utility::string_t(ss.str());
}
utility::string_t ModelBase::toString( const int64_t val )
{
utility::stringstream_t ss;
ss << val;
return utility::string_t(ss.str());
}
utility::string_t ModelBase::toString (const utility::string_t &val )
{
utility::stringstream_t ss;
ss << val;
return utility::string_t(ss.str());
}
utility::string_t ModelBase::toString( const utility::datetime &val )
{
return val.to_string(utility::datetime::ISO_8601);
}
utility::string_t ModelBase::toString( const web::json::value &val )
{
return val.serialize();
}
utility::string_t ModelBase::toString( const std::shared_ptr<HttpContent>& val )
{
utility::stringstream_t ss;
if( val != nullptr )
{
ss << val->getData();
}
return utility::string_t(ss.str());
}
web::json::value ModelBase::toJson(bool value)
{
return web::json::value::boolean(value);
}
web::json::value ModelBase::toJson( float value )
{
return web::json::value::number(value);
}
web::json::value ModelBase::toJson( double value )
{
return web::json::value::number(value);
}
web::json::value ModelBase::toJson( int32_t value )
{
return web::json::value::number(value);
}
web::json::value ModelBase::toJson( int64_t value )
{
return web::json::value::number(value);
}
web::json::value ModelBase::toJson( const utility::string_t& value )
{
return web::json::value::string(value);
}
web::json::value ModelBase::toJson( const utility::datetime& value )
{
return web::json::value::string(value.to_string(utility::datetime::ISO_8601));
}
web::json::value ModelBase::toJson( const web::json::value& value )
{
return value;
}
web::json::value ModelBase::toJson( const std::shared_ptr<HttpContent>& content )
{
web::json::value value;
if(content != nullptr)
{
value[utility::conversions::to_string_t("ContentDisposition")] = ModelBase::toJson(content->getContentDisposition());
value[utility::conversions::to_string_t("ContentType")] = ModelBase::toJson(content->getContentType());
value[utility::conversions::to_string_t("FileName")] = ModelBase::toJson(content->getFileName());
value[utility::conversions::to_string_t("InputStream")] = web::json::value::string( ModelBase::toBase64(content->getData()) );
}
return value;
}
web::json::value ModelBase::toJson( const std::shared_ptr<utility::datetime>& val )
{
web::json::value retVal;
if(val != nullptr)
{
retVal = toJson(*val);
}
return retVal;
}
bool ModelBase::fromString( const utility::string_t& val, bool &outVal )
{
utility::stringstream_t ss(val);
bool success = true;
try
{
ss >> outVal;
}
catch (...)
{
success = false;
}
return success;
}
bool ModelBase::fromString( const utility::string_t& val, float &outVal )
{
utility::stringstream_t ss(val);
bool success = true;
try
{
ss >> outVal;
}
catch (...)
{
int64_t intVal = 0;
success = ModelBase::fromString(val, intVal);
if(success)
{
outVal = static_cast<float>(intVal);
}
}
return success;
}
bool ModelBase::fromString( const utility::string_t& val, double &outVal )
{
utility::stringstream_t ss(val);
bool success = true;
try
{
ss >> outVal;
}
catch (...)
{
int64_t intVal = 0;
success = ModelBase::fromString(val, intVal);
if(success)
{
outVal = static_cast<double>(intVal);
}
}
return success;
}
bool ModelBase::fromString( const utility::string_t& val, int32_t &outVal )
{
utility::stringstream_t ss(val);
bool success = true;
try
{
ss >> outVal;
}
catch (...)
{
success = false;
}
return success;
}
bool ModelBase::fromString( const utility::string_t& val, int64_t &outVal )
{
utility::stringstream_t ss(val);
bool success = true;
try
{
ss >> outVal;
}
catch (...)
{
success = false;
}
return success;
}
bool ModelBase::fromString( const utility::string_t& val, utility::string_t &outVal )
{
utility::stringstream_t ss(val);
bool success = true;
try
{
ss >> outVal;
}
catch (...)
{
success = false;
}
return success;
}
bool ModelBase::fromString( const utility::string_t& val, utility::datetime &outVal )
{
bool success = true;
auto dt = utility::datetime::from_string(val, utility::datetime::ISO_8601);
if( dt.is_initialized() )
{
outVal = dt;
}
else
{
success = false;
}
return success;
}
bool ModelBase::fromString( const utility::string_t& val, web::json::value &outVal )
{
outVal = web::json::value::parse(val);
return !outVal.is_null();
}
bool ModelBase::fromString( const utility::string_t& val, std::shared_ptr<HttpContent>& outVal )
{
bool ok = true;
if(outVal == nullptr)
{
outVal = std::shared_ptr<HttpContent>(new HttpContent());
}
if(outVal != nullptr)
{
outVal->setData(std::shared_ptr<std::istream>(new std::stringstream(utility::conversions::to_utf8string(val))));
}
else
{
ok = false;
}
return ok;
}
bool ModelBase::fromString( const utility::string_t& val, std::shared_ptr<utility::datetime>& outVal )
{
bool ok = false;
if(outVal == nullptr)
{
outVal = std::shared_ptr<utility::datetime>(new utility::datetime());
}
if( outVal != nullptr )
{
ok = fromJson(web::json::value::parse(val), *outVal);
}
return ok;
}
bool ModelBase::fromJson( const web::json::value& val, bool & outVal )
{
outVal = !val.is_boolean() ? false : val.as_bool();
return val.is_boolean();
}
bool ModelBase::fromJson( const web::json::value& val, float & outVal )
{
outVal = (!val.is_double() && !val.is_integer()) ? std::numeric_limits<float>::quiet_NaN(): static_cast<float>(val.as_double());
return val.is_double() || val.is_integer();
}
bool ModelBase::fromJson( const web::json::value& val, double & outVal )
{
outVal = (!val.is_double() && !val.is_integer()) ? std::numeric_limits<double>::quiet_NaN(): val.as_double();
return val.is_double() || val.is_integer();
}
bool ModelBase::fromJson( const web::json::value& val, int32_t & outVal )
{
outVal = !val.is_integer() ? std::numeric_limits<int32_t>::quiet_NaN() : val.as_integer();
return val.is_integer();
}
bool ModelBase::fromJson( const web::json::value& val, int64_t & outVal )
{
outVal = !val.is_number() ? std::numeric_limits<int64_t>::quiet_NaN() : val.as_number().to_int64();
return val.is_number();
}
bool ModelBase::fromJson( const web::json::value& val, utility::string_t & outVal )
{
outVal = val.is_string() ? val.as_string() : utility::conversions::to_string_t("");
return val.is_string();
}
bool ModelBase::fromJson( const web::json::value& val, utility::datetime & outVal )
{
outVal = val.is_null() ? utility::datetime::from_string(utility::conversions::to_string_t("NULL"), utility::datetime::ISO_8601) : utility::datetime::from_string(val.as_string(), utility::datetime::ISO_8601);
return outVal.is_initialized();
}
bool ModelBase::fromJson( const web::json::value& val, web::json::value & outVal )
{
outVal = val;
return !val.is_null();
}
bool ModelBase::fromJson( const web::json::value& val, std::shared_ptr<HttpContent>& content )
{
bool result = false;
if( content != nullptr)
{
result = true;
if(content == nullptr)
{
content = std::shared_ptr<HttpContent>(new HttpContent());
}
if(val.has_field(utility::conversions::to_string_t("ContentDisposition")))
{
utility::string_t value;
result = result && ModelBase::fromJson(val.at(utility::conversions::to_string_t("ContentDisposition")), value);
content->setContentDisposition( value );
}
if(val.has_field(utility::conversions::to_string_t("ContentType")))
{
utility::string_t value;
result = result && ModelBase::fromJson(val.at(utility::conversions::to_string_t("ContentType")), value);
content->setContentType( value );
}
if(val.has_field(utility::conversions::to_string_t("FileName")))
{
utility::string_t value;
result = result && ModelBase::fromJson(val.at(utility::conversions::to_string_t("FileName")), value);
content->setFileName( value );
}
if(val.has_field(utility::conversions::to_string_t("InputStream")))
{
utility::string_t value;
result = result && ModelBase::fromJson(val.at(utility::conversions::to_string_t("InputStream")), value);
content->setData( ModelBase::fromBase64( value ) );
}
}
return result;
}
bool ModelBase::fromJson( const web::json::value& val, std::shared_ptr<utility::datetime> &outVal )
{
bool ok = false;
if(outVal == nullptr)
{
outVal = std::shared_ptr<utility::datetime>(new utility::datetime());
}
if( outVal != nullptr )
{
ok = fromJson(val, *outVal);
}
return ok;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, bool value, const utility::string_t& contentType )
{
std::shared_ptr<HttpContent> content( new HttpContent );
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
std::stringstream* valueAsStringStream = new std::stringstream();
(*valueAsStringStream) << value;
content->setData( std::shared_ptr<std::istream>( valueAsStringStream ) );
return content;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, float value, const utility::string_t& contentType )
{
std::shared_ptr<HttpContent> content( new HttpContent );
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
std::stringstream* valueAsStringStream = new std::stringstream();
(*valueAsStringStream) << value;
content->setData( std::shared_ptr<std::istream>( valueAsStringStream ) );
return content;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, double value, const utility::string_t& contentType )
{
std::shared_ptr<HttpContent> content( new HttpContent );
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
std::stringstream* valueAsStringStream = new std::stringstream();
(*valueAsStringStream) << value;
content->setData( std::shared_ptr<std::istream>( valueAsStringStream ) );
return content;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, int32_t value, const utility::string_t& contentType )
{
std::shared_ptr<HttpContent> content( new HttpContent );
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
std::stringstream* valueAsStringStream = new std::stringstream();
(*valueAsStringStream) << value;
content->setData( std::shared_ptr<std::istream>( valueAsStringStream ) );
return content;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, int64_t value, const utility::string_t& contentType )
{
std::shared_ptr<HttpContent> content( new HttpContent );
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
std::stringstream* valueAsStringStream = new std::stringstream();
(*valueAsStringStream) << value;
content->setData( std::shared_ptr<std::istream>( valueAsStringStream) ) ;
return content;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const utility::string_t& value, const utility::string_t& contentType)
{
std::shared_ptr<HttpContent> content(new HttpContent);
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
content->setData( std::shared_ptr<std::istream>( new std::stringstream( utility::conversions::to_utf8string(value) ) ) );
return content;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const utility::datetime& value, const utility::string_t& contentType )
{
std::shared_ptr<HttpContent> content( new HttpContent );
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
content->setData( std::shared_ptr<std::istream>( new std::stringstream( utility::conversions::to_utf8string(value.to_string(utility::datetime::ISO_8601) ) ) ) );
return content;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const web::json::value& value, const utility::string_t& contentType )
{
std::shared_ptr<HttpContent> content( new HttpContent );
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
content->setData( std::shared_ptr<std::istream>( new std::stringstream( utility::conversions::to_utf8string(value.serialize()) ) ) );
return content;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const std::shared_ptr<HttpContent>& value )
{
std::shared_ptr<HttpContent> content( new HttpContent );
if( value != nullptr )
{
content->setName( name );
content->setContentDisposition( value->getContentDisposition() );
content->setContentType( value->getContentType() );
content->setData( value->getData() );
content->setFileName( value->getFileName() );
}
return content;
}
std::shared_ptr<HttpContent> ModelBase::toHttpContent(const utility::string_t& name, const std::shared_ptr<utility::datetime>& value , const utility::string_t& contentType )
{
std::shared_ptr<HttpContent> content( new HttpContent );
if (value != nullptr )
{
content->setName( name );
content->setContentDisposition( utility::conversions::to_string_t("form-data") );
content->setContentType( contentType );
content->setData( std::shared_ptr<std::istream>( new std::stringstream( utility::conversions::to_utf8string( toJson(*value).serialize() ) ) ) );
}
return content;
}
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, bool & outVal )
{
utility::string_t str;
if( val == nullptr ) return false;
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, float & outVal )
{
utility::string_t str;
if( val == nullptr ) return false;
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, double & outVal )
{
utility::string_t str;
if( val == nullptr ) return false;
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, int32_t & outVal )
{
utility::string_t str;
if( val == nullptr ) return false;
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, int64_t & outVal )
{
utility::string_t str;
if( val == nullptr ) return false;
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, utility::string_t & outVal )
{
if( val == nullptr ) return false;
std::shared_ptr<std::istream> data = val->getData();
data->seekg( 0, data->beg );
std::string str((std::istreambuf_iterator<char>(*data.get())),
std::istreambuf_iterator<char>());
outVal = utility::conversions::to_string_t(str);
return true;
}
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, utility::datetime & outVal )
{
utility::string_t str;
if( val == nullptr ) return false;
ModelBase::fromHttpContent(val, str);
outVal = utility::datetime::from_string(str, utility::datetime::ISO_8601);
return true;
}
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, web::json::value & outVal )
{
utility::string_t str;
if( val == nullptr ) return false;
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, std::shared_ptr<HttpContent>& outVal )
{
utility::string_t str;
if( val == nullptr ) return false;
if( outVal == nullptr )
{
outVal = std::shared_ptr<HttpContent>(new HttpContent());
}
ModelBase::fromHttpContent(val, str);
return fromString(str, outVal);
}
// base64 encoding/decoding based on : https://en.wikibooks.org/wiki/Algorithm_Implementation/Miscellaneous/Base64#C.2B.2B
const static char Base64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const static char Base64PadChar = '=';
utility::string_t ModelBase::toBase64( utility::string_t value )
{
std::shared_ptr<std::istream> source( new std::stringstream( utility::conversions::to_utf8string(value) ) );
return ModelBase::toBase64(source);
}
utility::string_t ModelBase::toBase64( std::shared_ptr<std::istream> value )
{
value->seekg( 0, value->end );
size_t length = value->tellg();
value->seekg( 0, value->beg );
utility::string_t base64;
base64.reserve( ((length / 3) + (length % 3 > 0)) * 4 );
char read[3] = { 0 };
uint32_t temp;
for ( size_t idx = 0; idx < length / 3; idx++ )
{
value->read( read, 3 );
temp = (read[0]) << 16;
temp += (read[1]) << 8;
temp += (read[2]);
base64.append( 1, Base64Chars[(temp & 0x00FC0000) >> 18] );
base64.append( 1, Base64Chars[(temp & 0x0003F000) >> 12] );
base64.append( 1, Base64Chars[(temp & 0x00000FC0) >> 6] );
base64.append( 1, Base64Chars[(temp & 0x0000003F)] );
}
switch ( length % 3 )
{
case 1:
value->read( read, 1 );
temp = read[0] << 16;
base64.append( 1, Base64Chars[(temp & 0x00FC0000) >> 18] );
base64.append( 1, Base64Chars[(temp & 0x0003F000) >> 12] );
base64.append( 2, Base64PadChar );
break;
case 2:
value->read( read, 2 );
temp = read[0] << 16;
temp += read[1] << 8;
base64.append( 1, Base64Chars[(temp & 0x00FC0000) >> 18] );
base64.append( 1, Base64Chars[(temp & 0x0003F000) >> 12] );
base64.append( 1, Base64Chars[(temp & 0x00000FC0) >> 6] );
base64.append( 1, Base64PadChar );
break;
}
return base64;
}
std::shared_ptr<std::istream> ModelBase::fromBase64( const utility::string_t& encoded )
{
std::shared_ptr<std::stringstream> result(new std::stringstream);
char outBuf[3] = { 0 };
uint32_t temp = 0;
utility::string_t::const_iterator cursor = encoded.begin();
while ( cursor < encoded.end() )
{
for ( size_t quantumPosition = 0; quantumPosition < 4; quantumPosition++ )
{
temp <<= 6;
if ( *cursor >= 0x41 && *cursor <= 0x5A )
{
temp |= *cursor - 0x41;
}
else if ( *cursor >= 0x61 && *cursor <= 0x7A )
{
temp |= *cursor - 0x47;
}
else if ( *cursor >= 0x30 && *cursor <= 0x39 )
{
temp |= *cursor + 0x04;
}
else if ( *cursor == 0x2B )
{
temp |= 0x3E; //change to 0x2D for URL alphabet
}
else if ( *cursor == 0x2F )
{
temp |= 0x3F; //change to 0x5F for URL alphabet
}
else if ( *cursor == Base64PadChar ) //pad
{
switch ( encoded.end() - cursor )
{
case 1: //One pad character
outBuf[0] = (temp >> 16) & 0x000000FF;
outBuf[1] = (temp >> 8) & 0x000000FF;
result->write( outBuf, 2 );
return result;
case 2: //Two pad characters
outBuf[0] = (temp >> 10) & 0x000000FF;
result->write( outBuf, 1 );
return result;
default:
throw web::json::json_exception( utility::conversions::to_string_t( "Invalid Padding in Base 64!" ).c_str() );
}
}
else
{
throw web::json::json_exception( utility::conversions::to_string_t( "Non-Valid Character in Base 64!" ).c_str() );
}
++cursor;
}
outBuf[0] = (temp >> 16) & 0x000000FF;
outBuf[1] = (temp >> 8) & 0x000000FF;
outBuf[2] = (temp) & 0x000000FF;
result->write( outBuf, 3 );
}
return result;
}
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}

View file

@ -0,0 +1,49 @@
{{>licenseInfo}}
/*
* MultipartFormData.h
*
* This class represents a container for building application/x-multipart-formdata requests.
*/
#ifndef {{modelHeaderGuardPrefix}}_MultipartFormData_H_
#define {{modelHeaderGuardPrefix}}_MultipartFormData_H_
{{{defaultInclude}}}
#include "{{packageName}}/IHttpBody.h"
#include "{{packageName}}/HttpContent.h"
#include <cpprest/details/basic_types.h>
#include <vector>
#include <map>
#include <memory>
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
class {{declspec}} MultipartFormData
: public IHttpBody
{
public:
MultipartFormData();
MultipartFormData(const utility::string_t& boundary);
virtual ~MultipartFormData();
virtual void add( std::shared_ptr<HttpContent> content );
virtual utility::string_t getBoundary();
virtual std::shared_ptr<HttpContent> getContent(const utility::string_t& name) const;
virtual bool hasContent(const utility::string_t& name) const;
virtual void writeTo( std::ostream& target );
protected:
std::vector<std::shared_ptr<HttpContent>> m_Contents;
utility::string_t m_Boundary;
std::map<utility::string_t, std::shared_ptr<HttpContent>> m_ContentLookup;
};
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}
#endif /* {{modelHeaderGuardPrefix}}_MultipartFormData_H_ */

View file

@ -0,0 +1,100 @@
{{>licenseInfo}}
#include "{{packageName}}/MultipartFormData.h"
#include "{{packageName}}/ModelBase.h"
#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/uuid_io.hpp>
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
MultipartFormData::MultipartFormData()
{
utility::stringstream_t uuidString;
uuidString << boost::uuids::random_generator()();
m_Boundary = uuidString.str();
}
MultipartFormData::MultipartFormData(const utility::string_t& boundary)
: m_Boundary(boundary)
{
}
MultipartFormData::~MultipartFormData()
{
}
utility::string_t MultipartFormData::getBoundary()
{
return m_Boundary;
}
void MultipartFormData::add( std::shared_ptr<HttpContent> content )
{
m_Contents.push_back( content );
m_ContentLookup[content->getName()] = content;
}
bool MultipartFormData::hasContent(const utility::string_t& name) const
{
return m_ContentLookup.find(name) != m_ContentLookup.end();
}
std::shared_ptr<HttpContent> MultipartFormData::getContent(const utility::string_t& name) const
{
auto result = m_ContentLookup.find(name);
if(result == m_ContentLookup.end())
{
return std::shared_ptr<HttpContent>(nullptr);
}
return result->second;
}
void MultipartFormData::writeTo( std::ostream& target )
{
for ( size_t i = 0; i < m_Contents.size(); i++ )
{
std::shared_ptr<HttpContent> content = m_Contents[i];
// boundary
target << "\r\n" << "--" << utility::conversions::to_utf8string( m_Boundary ) << "\r\n";
// headers
target << "Content-Disposition: " << utility::conversions::to_utf8string( content->getContentDisposition() );
if ( content->getName().size() > 0 )
{
target << "; name=\"" << utility::conversions::to_utf8string( content->getName() ) << "\"";
}
if ( content->getFileName().size() > 0 )
{
target << "; filename=\"" << utility::conversions::to_utf8string( content->getFileName() ) << "\"";
}
target << "\r\n";
if ( content->getContentType().size() > 0 )
{
target << "Content-Type: " << utility::conversions::to_utf8string( content->getContentType() ) << "\r\n";
}
target << "\r\n";
// body
std::shared_ptr<std::istream> data = content->getData();
data->seekg( 0, data->end );
std::vector<char> dataBytes( data->tellg() );
data->seekg( 0, data->beg );
data->read( &dataBytes[0], dataBytes.size() );
std::copy( dataBytes.begin(), dataBytes.end(), std::ostreambuf_iterator<char>( target ) );
}
target << "\r\n--" << utility::conversions::to_utf8string( m_Boundary ) << "--\r\n";
}
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}

View file

@ -0,0 +1,50 @@
{{>licenseInfo}}
/*
* Object.h
*
* This is the implementation of a JSON object.
*/
#ifndef {{modelHeaderGuardPrefix}}_Object_H_
#define {{modelHeaderGuardPrefix}}_Object_H_
{{{defaultInclude}}}
#include "{{packageName}}/ModelBase.h"
#include <cpprest/details/basic_types.h>
#include <cpprest/json.h>
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
class {{declspec}} Object : public ModelBase
{
public:
Object();
virtual ~Object();
/////////////////////////////////////////////
/// ModelBase overrides
void validate() override;
web::json::value toJson() const override;
bool fromJson(const web::json::value& json) override;
void toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) const override;
bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) override;
/////////////////////////////////////////////
/// Object manipulation
web::json::value getValue(const utility::string_t& key) const;
void setValue(const utility::string_t& key, const web::json::value& value);
private:
web::json::value m_object;
};
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}
#endif /* {{modelHeaderGuardPrefix}}_Object_H_ */

View file

@ -0,0 +1,79 @@
{{>licenseInfo}}
#include "{{packageName}}/Object.h"
{{#modelNamespaceDeclarations}}
namespace {{this}} {
{{/modelNamespaceDeclarations}}
Object::Object()
{
m_object = web::json::value::object();
}
Object::~Object()
{
}
void Object::validate()
{
}
web::json::value Object::toJson() const
{
return m_object;
}
bool Object::fromJson(const web::json::value& val)
{
if (val.is_object())
{
m_object = val;
m_IsSet = true;
}
return isSet();
}
void Object::toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) const
{
utility::string_t namePrefix = prefix;
if(namePrefix.size() > 0 && namePrefix.substr(namePrefix.size() - 1) != utility::conversions::to_string_t("."))
{
namePrefix += utility::conversions::to_string_t(".");
}
multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t("object"), m_object));
}
bool Object::fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix)
{
utility::string_t namePrefix = prefix;
if(namePrefix.size() > 0 && namePrefix.substr(namePrefix.size() - 1) != utility::conversions::to_string_t("."))
{
namePrefix += utility::conversions::to_string_t(".");
}
if( ModelBase::fromHttpContent(multipart->getContent(namePrefix + utility::conversions::to_string_t("object")), m_object ) )
{
m_IsSet = true;
}
return isSet();
}
web::json::value Object::getValue(const utility::string_t& key) const
{
return m_object.at(key);
}
void Object::setValue(const utility::string_t& key, const web::json::value& value)
{
if( !value.is_null() )
{
m_object[key] = value;
m_IsSet = true;
}
}
{{#modelNamespaceDeclarations}}
}
{{/modelNamespaceDeclarations}}

View file

@ -639,20 +639,13 @@ components:
required: required:
- id - id
- name - name
Tag:
description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names'
type: object
example:
en: Shojo
ru: Сёдзё
ja: 少女
additionalProperties:
type: string
Tags: Tags:
description: Array of localized tags description: Array of localized tags
type: array type: array
items: items:
$ref: '#/components/schemas/Tag' type: object
additionalProperties:
type: string
example: example:
- en: Shojo - en: Shojo
ru: Сёдзё ru: Сёдзё
@ -800,7 +793,7 @@ components:
review_id: review_id:
type: integer type: integer
format: int64 format: int64
ftime: ctime:
type: string type: string
format: date-time format: date-time
required: required:
@ -824,7 +817,7 @@ components:
review_id: review_id:
type: integer type: integer
format: int64 format: int64
ftime: ctime:
type: string type: string
format: date-time format: date-time
required: required:

View file

@ -156,7 +156,7 @@ type User struct {
// UserTitle defines model for UserTitle. // UserTitle defines model for UserTitle.
type UserTitle struct { type UserTitle struct {
Ftime *time.Time `json:"ftime,omitempty"` Ctime *time.Time `json:"ctime,omitempty"`
Rate *int32 `json:"rate,omitempty"` Rate *int32 `json:"rate,omitempty"`
ReviewId *int64 `json:"review_id,omitempty"` ReviewId *int64 `json:"review_id,omitempty"`
@ -168,7 +168,7 @@ type UserTitle struct {
// UserTitleMini defines model for UserTitleMini. // UserTitleMini defines model for UserTitleMini.
type UserTitleMini struct { type UserTitleMini struct {
Ftime *time.Time `json:"ftime,omitempty"` Ctime *time.Time `json:"ctime,omitempty"`
Rate *int32 `json:"rate,omitempty"` Rate *int32 `json:"rate,omitempty"`
ReviewId *int64 `json:"review_id,omitempty"` ReviewId *int64 `json:"review_id,omitempty"`

View file

@ -17,6 +17,6 @@ properties:
review_id: review_id:
type: integer type: integer
format: int64 format: int64
ftime: ctime:
type: string type: string
format: date-time format: date-time

View file

@ -18,6 +18,6 @@ properties:
review_id: review_id:
type: integer type: integer
format: int64 format: int64
ftime: ctime:
type: string type: string
format: date-time format: date-time

View file

@ -56,12 +56,6 @@ type ServerInterface interface {
// Get service impersontaion token // Get service impersontaion token
// (POST /get-impersonation-token) // (POST /get-impersonation-token)
GetImpersonationToken(c *gin.Context) GetImpersonationToken(c *gin.Context)
// Logs out the user
// (POST /logout)
Logout(c *gin.Context)
// Refreshes access_token and refresh_token
// (GET /refresh-tokens)
RefreshTokens(c *gin.Context)
// Sign in a user and return JWT // Sign in a user and return JWT
// (POST /sign-in) // (POST /sign-in)
PostSignIn(c *gin.Context) PostSignIn(c *gin.Context)
@ -94,32 +88,6 @@ func (siw *ServerInterfaceWrapper) GetImpersonationToken(c *gin.Context) {
siw.Handler.GetImpersonationToken(c) siw.Handler.GetImpersonationToken(c)
} }
// Logout operation middleware
func (siw *ServerInterfaceWrapper) Logout(c *gin.Context) {
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.Logout(c)
}
// RefreshTokens operation middleware
func (siw *ServerInterfaceWrapper) RefreshTokens(c *gin.Context) {
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.RefreshTokens(c)
}
// PostSignIn operation middleware // PostSignIn operation middleware
func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) { func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) {
@ -174,18 +142,10 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
} }
router.POST(options.BaseURL+"/get-impersonation-token", wrapper.GetImpersonationToken) router.POST(options.BaseURL+"/get-impersonation-token", wrapper.GetImpersonationToken)
router.POST(options.BaseURL+"/logout", wrapper.Logout)
router.GET(options.BaseURL+"/refresh-tokens", wrapper.RefreshTokens)
router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn) router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn)
router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp) router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp)
} }
type ClientErrorResponse struct {
}
type ServerErrorResponse struct {
}
type UnauthorizedErrorResponse struct { type UnauthorizedErrorResponse struct {
} }
@ -216,64 +176,6 @@ func (response GetImpersonationToken401Response) VisitGetImpersonationTokenRespo
return nil return nil
} }
type LogoutRequestObject struct {
}
type LogoutResponseObject interface {
VisitLogoutResponse(w http.ResponseWriter) error
}
type Logout200Response struct {
}
func (response Logout200Response) VisitLogoutResponse(w http.ResponseWriter) error {
w.WriteHeader(200)
return nil
}
type Logout500Response = ServerErrorResponse
func (response Logout500Response) VisitLogoutResponse(w http.ResponseWriter) error {
w.WriteHeader(500)
return nil
}
type RefreshTokensRequestObject struct {
}
type RefreshTokensResponseObject interface {
VisitRefreshTokensResponse(w http.ResponseWriter) error
}
type RefreshTokens200Response struct {
}
func (response RefreshTokens200Response) VisitRefreshTokensResponse(w http.ResponseWriter) error {
w.WriteHeader(200)
return nil
}
type RefreshTokens400Response = ClientErrorResponse
func (response RefreshTokens400Response) VisitRefreshTokensResponse(w http.ResponseWriter) error {
w.WriteHeader(400)
return nil
}
type RefreshTokens401Response = UnauthorizedErrorResponse
func (response RefreshTokens401Response) VisitRefreshTokensResponse(w http.ResponseWriter) error {
w.WriteHeader(401)
return nil
}
type RefreshTokens500Response = ServerErrorResponse
func (response RefreshTokens500Response) VisitRefreshTokensResponse(w http.ResponseWriter) error {
w.WriteHeader(500)
return nil
}
type PostSignInRequestObject struct { type PostSignInRequestObject struct {
Body *PostSignInJSONRequestBody Body *PostSignInJSONRequestBody
} }
@ -325,12 +227,6 @@ type StrictServerInterface interface {
// Get service impersontaion token // Get service impersontaion token
// (POST /get-impersonation-token) // (POST /get-impersonation-token)
GetImpersonationToken(ctx context.Context, request GetImpersonationTokenRequestObject) (GetImpersonationTokenResponseObject, error) GetImpersonationToken(ctx context.Context, request GetImpersonationTokenRequestObject) (GetImpersonationTokenResponseObject, error)
// Logs out the user
// (POST /logout)
Logout(ctx context.Context, request LogoutRequestObject) (LogoutResponseObject, error)
// Refreshes access_token and refresh_token
// (GET /refresh-tokens)
RefreshTokens(ctx context.Context, request RefreshTokensRequestObject) (RefreshTokensResponseObject, error)
// Sign in a user and return JWT // Sign in a user and return JWT
// (POST /sign-in) // (POST /sign-in)
PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error) PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error)
@ -384,56 +280,6 @@ func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) {
} }
} }
// Logout operation middleware
func (sh *strictHandler) Logout(ctx *gin.Context) {
var request LogoutRequestObject
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.Logout(ctx, request.(LogoutRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Logout")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(LogoutResponseObject); ok {
if err := validResponse.VisitLogoutResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}
// RefreshTokens operation middleware
func (sh *strictHandler) RefreshTokens(ctx *gin.Context) {
var request RefreshTokensRequestObject
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.RefreshTokens(ctx, request.(RefreshTokensRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "RefreshTokens")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(RefreshTokensResponseObject); ok {
if err := validResponse.VisitRefreshTokensResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}
// PostSignIn operation middleware // PostSignIn operation middleware
func (sh *strictHandler) PostSignIn(ctx *gin.Context) { func (sh *strictHandler) PostSignIn(ctx *gin.Context) {
var request PostSignInRequestObject var request PostSignInRequestObject

View file

@ -1,9 +0,0 @@
package auth
import "github.com/golang-jwt/jwt/v5"
type TokenClaims struct {
Type string `json:"type"`
ImpID *string `json:"imp_id,omitempty"`
jwt.RegisteredClaims
}

View file

@ -116,33 +116,6 @@ paths:
"401": "401":
$ref: '#/components/responses/UnauthorizedError' $ref: '#/components/responses/UnauthorizedError'
/refresh-tokens:
get:
summary: Refreshes access_token and refresh_token
operationId: refreshTokens
tags: [Auth]
responses:
# This one sets two cookies: access_token and refresh_token
"200":
description: Refresh success
"400":
$ref: '#/components/responses/ClientError'
"401":
$ref: '#/components/responses/UnauthorizedError'
"500":
$ref: '#/components/responses/ServerError'
/logout:
post:
summary: Logs out the user
operationId: logout
tags: [Auth]
responses:
"200":
description: Logout success
"500":
$ref: '#/components/responses/ServerError'
components: components:
securitySchemes: securitySchemes:
bearerAuth: bearerAuth:
@ -150,8 +123,4 @@ components:
scheme: bearer scheme: bearer
responses: responses:
UnauthorizedError: UnauthorizedError:
description: Access token is missing or invalid description: Access token is missing or invalid
ServerError:
description: ServerError
ClientError:
description: ClientError

View file

@ -67,9 +67,6 @@ services:
RABBITMQ_URL: ${RABBITMQ_URL} RABBITMQ_URL: ${RABBITMQ_URL}
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY} JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
AUTH_ENABLED: ${AUTH_ENABLED} AUTH_ENABLED: ${AUTH_ENABLED}
IMAGES_BASE_URL: http://nyanimedb-images:8000
ports:
- "8080:8080"
# ports: # ports:
# - "8080:8080" # - "8080:8080"
depends_on: depends_on:
@ -105,43 +102,9 @@ services:
networks: networks:
- nyanimedb-network - nyanimedb-network
nyanimedb-etl:
image: meowgit.nekoea.red/nihonium/nyanimedb-etl:latest
container_name: nyanimedb-etl
restart: always
environment:
DATABASE_URL: ${DATABASE_URL}
RABBITMQ_URL: ${RABBITMQ_URL}
NYANIMEDB_IMPORT_RPC_QUEUE: anime_import_rpc
NYANIMEDB_IMAGE_SERVICE_URL: http://nyanimedb-images:8000
depends_on:
- postgres
- rabbitmq
- nyanimedb-images
networks:
- nyanimedb-network
nyanimedb-images:
image: meowgit.nekoea.red/nihonium/nyanimedb-image-storage:latest
container_name: nyanimedb-images
restart: always
environment:
NYANIMEDB_MEDIA_ROOT: /media
volumes:
- media_data:/media
networks:
- nyanimedb-network
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/docs || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes: volumes:
postgres_data: postgres_data:
rabbitmq_data: rabbitmq_data:
media_data:
redis_data: redis_data:
networks: networks:

View file

@ -1,2 +0,0 @@
.venv
__pycache__

View file

@ -1 +0,0 @@
3.12

View file

@ -1,26 +0,0 @@
# anime_etl/canonicalizer.py
from __future__ import annotations
from models import SourceTitle, CanonicalTitle
def source_title_to_canonical(src: SourceTitle) -> CanonicalTitle:
return CanonicalTitle(
id=None,
title_names=src.title_names,
studio=src.studio,
tags=list(src.tags),
poster=src.poster,
title_status=src.title_status,
rating=src.rating,
rating_count=src.rating_count,
release_year=src.release_year,
release_season=src.release_season,
season=src.season,
episodes_aired=src.episodes_aired,
episodes_all=src.episodes_all,
episodes_len=src.episodes_len,
)

View file

@ -1,254 +0,0 @@
# anime_etl/db/repository.py
from __future__ import annotations
import json
from typing import Optional, Dict, List
import psycopg
from psycopg.rows import dict_row
from models import CanonicalTitle, Studio, Image
from images.downloader import ensure_image_downloaded
Conn = psycopg.AsyncConnection
def _choose_primary_name(
title_names: Dict[str, List[str]],
) -> Optional[tuple[str, str]]:
# (lang, name)
for lang in ("en", "romaji", "ja"):
variants = title_names.get(lang) or []
if variants:
return lang, variants[0]
for lang, variants in title_names.items():
if variants:
return lang, variants[0]
return None
import re
_SHA1_PATH_RE = re.compile(
r"^[a-zA-Z0-9_-]+/[0-9a-f]{2}/[0-9a-f]{2}/[0-9a-f]{40}\.[a-zA-Z0-9]{1,5}$"
)
def is_normalized_image_path(path: str) -> bool:
return bool(_SHA1_PATH_RE.match(path))
async def get_or_create_image(
conn: Conn,
img: Optional[Image],
*,
subdir: str = "posters",
) -> Optional[int]:
if img is None or not img.image_path:
return None
url = img.image_path
# 1) Если это URL → скачиваем через image-service
if url.startswith("http://") or url.startswith("https://"):
try:
rel_path = await ensure_image_downloaded(url, subdir=subdir)
except Exception as e:
# не удалось скачать картинку — просто пропускаем
return None
else:
# неправильный формат — просто пропуск
return None
# 3) Проверка в базе
async with conn.cursor(row_factory=dict_row) as cur:
await cur.execute(
"SELECT id FROM images WHERE image_path = %s",
(rel_path,),
)
row = await cur.fetchone()
if row:
return row["id"]
# 4) Вставляем запись
await cur.execute(
"""
INSERT INTO images (storage_type, image_path)
VALUES (%s, %s)
RETURNING id
""",
("local", rel_path),
)
row = await cur.fetchone()
return row["id"]
async def get_or_create_studio(
conn: Conn,
studio: Optional[Studio],
) -> Optional[int]:
if studio is None or not studio.name:
return None
async with conn.cursor(row_factory=dict_row) as cur:
# 1. Сначала ищем студию
await cur.execute(
"SELECT id, illust_id, studio_desc FROM studios WHERE studio_name = %s",
(studio.name,),
)
row = await cur.fetchone()
if row:
studio_id = row["id"]
illust_id = row["illust_id"]
studio_desc = row["studio_desc"]
# 1a. Если нет illust_id, а нам пришёл постер — докачаем и обновим
if illust_id is None and studio.poster is not None:
illust_id = await get_or_create_image(conn, studio.poster, subdir="studios")
await cur.execute(
"UPDATE studios SET illust_id = %s WHERE id = %s",
(illust_id, studio_id),
)
# 1b. Если нет описания, а enrich уже поднял description — обновим описание
if studio_desc is None and studio.description:
await cur.execute(
"UPDATE studios SET studio_desc = %s WHERE id = %s",
(studio.description, studio_id),
)
return studio_id
# 2. Студии нет — создаём
illust_id: Optional[int] = None
if studio.poster is not None:
illust_id = await get_or_create_image(conn, studio.poster, subdir="studios")
await cur.execute(
"""
INSERT INTO studios (studio_name, illust_id, studio_desc)
VALUES (%s, %s, %s)
RETURNING id
""",
(studio.name, illust_id, studio.description),
)
row = await cur.fetchone()
return row["id"]
async def find_title_id_by_name_and_year(
conn: Conn,
title_names: Dict[str, List[str]],
release_year: Optional[int],
) -> Optional[int]:
if release_year is None:
return None
pair = _choose_primary_name(title_names)
if not pair:
return None
lang, primary_name = pair
probe = json.dumps({lang: [primary_name]})
async with conn.cursor(row_factory=dict_row) as cur:
await cur.execute(
"""
SELECT id
FROM titles
WHERE release_year = %s
AND title_names @> %s::jsonb
LIMIT 1
""",
(release_year, probe),
)
row = await cur.fetchone()
if not row:
return None
return row["id"]
async def insert_title(
conn: Conn,
title: CanonicalTitle,
studio_id: Optional[int],
poster_id: Optional[int],
) -> int:
episodes_len_json = (
json.dumps(title.episodes_len) if title.episodes_len is not None else None
)
async with conn.cursor(row_factory=dict_row) as cur:
await cur.execute(
"""
INSERT INTO titles (
title_names,
studio_id,
poster_id,
title_status,
rating,
rating_count,
release_year,
release_season,
season,
episodes_aired,
episodes_all,
episodes_len
)
VALUES (
%s::jsonb,
%s,
%s,
%s,
%s,
%s,
%s,
%s,
%s,
%s,
%s,
%s::jsonb
)
RETURNING id
""",
(
json.dumps(title.title_names),
studio_id,
poster_id,
title.title_status,
title.rating,
title.rating_count,
title.release_year,
title.release_season,
title.season,
title.episodes_aired,
title.episodes_all,
episodes_len_json,
),
)
row = await cur.fetchone()
return row["id"]
async def insert_title_if_not_exists(
conn: Conn,
title: CanonicalTitle,
studio_id: Optional[int],
poster_id: Optional[int],
) -> int:
existing_id = await find_title_id_by_name_and_year(
conn,
title.title_names,
title.release_year,
)
if existing_id is not None:
return existing_id
return await insert_title(conn, title, studio_id, poster_id)

View file

@ -1,35 +0,0 @@
# anime_etl/images/downloader.py
from __future__ import annotations
import os
from typing import Final
import httpx
IMAGE_SERVICE_URL: Final[str] = os.getenv(
"NYANIMEDB_IMAGE_SERVICE_URL",
"http://127.0.0.1:8000"
)
async def ensure_image_downloaded(url: str, subdir: str = "posters") -> str:
"""
Просит image-service скачать картинку по URL и сохранить её у себя.
Возвращает относительный путь (subdir/ab/cd/<sha1>.ext),
который можно писать в images.image_path.
"""
async with httpx.AsyncClient(timeout=20.0) as client:
resp = await client.post(
f"{IMAGE_SERVICE_URL}/download-by-url",
json={"url": url, "subdir": subdir},
)
resp.raise_for_status()
data = resp.json()
# ожидаем {"path": "..."}
path = data["path"]
if not isinstance(path, str):
raise RuntimeError(f"Invalid response from image service: {data!r}")
return path

View file

@ -1,38 +0,0 @@
from __future__ import annotations
from typing import Optional
from models import Studio
from sources.jikan_async_client import search_producer, fetch_producer_full
async def enrich_studio_with_jikan_desc(studio: Studio) -> Studio:
"""
Если у студии нет description ищем её в Jikan и подтягиваем about.
Ничего не ломает:
- если не нашли / нет about возвращаем студию как есть
- poster/id не трогаем
"""
if not studio or studio.description:
return studio
matches = await search_producer(studio.name, limit=1)
if not matches:
return studio
mal_id = matches[0].get("mal_id")
if not isinstance(mal_id, int):
return studio
full = await fetch_producer_full(mal_id)
if not full:
return studio
about = full.get("about")
if not isinstance(about, str) or not about.strip():
return studio
# лёгкая нормализация: убираем лишние переносы/пробелы
studio.description = " ".join(about.split())
return studio

View file

@ -1,35 +0,0 @@
SEASON = {
"winter": "WINTER",
"spring": "SPRING",
"summer": "SUMMER",
"fall": "FALL",
}
FORMAT = {
"tv": "TV",
"movie": "MOVIE",
"ova": "OVA",
"ona": "ONA",
"special": "SPECIAL",
"music": "MUSIC",
}
def to_anilist_filters(local: dict) -> dict:
"""Наши фильтры → AniList GraphQL variables."""
q = local.get("query")
year = local.get("year")
season = SEASON.get(local.get("season"))
fmt = FORMAT.get(local.get("type"))
limit = local.get("limit", 10)
variables = {
"page": 1,
"perPage": limit,
}
if q: variables["search"] = q
if year: variables["seasonYear"] = year
if season: variables["season"] = season
if fmt: variables["format"] = fmt
return variables

View file

@ -1,82 +0,0 @@
# anime_etl/models.py
from __future__ import annotations
from enum import Enum
from typing import Dict, List, Optional
from pydantic import BaseModel
class Source(str, Enum):
ANILIST = "anilist"
JIKAN = "jikan"
SHIKIMORI = "shikimori"
class Studio(BaseModel):
id: Optional[int] = None
name: str
poster: Optional["Image"] = None
description: Optional[str] = None
class Image(BaseModel):
id: Optional[int] = None
storage_type: Optional[str] = None # для БД: storage_type_t
image_path: str # для внешнего постера URL
class Tag(BaseModel):
# локализованный тег, как в Tag.yaml: { "en": "Shojo", "ru": "Сёдзё", ... }
names: Dict[str, str]
class SourceTitle(BaseModel):
"""
Нормализованная инфа из одного источника (AniList/Jikan/...).
"""
source: Source
external_id: str
title_names: Dict[str, List[str]]
studio: Optional[Studio] = None
tags: List[Tag] = []
poster: Optional[Image] = None
title_status: Optional[str] = None
rating: Optional[float] = None
rating_count: Optional[int] = None
release_year: Optional[int] = None
release_season: Optional[str] = None
season: Optional[int] = None
episodes_aired: Optional[int] = None
episodes_all: Optional[int] = None
episodes_len: Optional[Dict[str, float]] = None
class CanonicalTitle(BaseModel):
"""
То, что пойдёт в нашу БД + будет соответствовать Title.yaml.
"""
id: Optional[int] = None
title_names: Dict[str, List[str]]
studio: Optional[Studio] = None
tags: List[Tag] = []
poster: Optional[Image] = None
title_status: Optional[str] = None
rating: Optional[float] = None
rating_count: Optional[int] = None
release_year: Optional[int] = None
release_season: Optional[str] = None
season: Optional[int] = None
episodes_aired: Optional[int] = None
episodes_all: Optional[int] = None
episodes_len: Optional[Dict[str, float]] = None

View file

@ -1,184 +0,0 @@
# anime_etl/anilist_normalizer.py
from __future__ import annotations
from typing import Any, Dict, List, Optional
from models import Source, SourceTitle, Studio, Image, Tag
from utils.season_resolver import resolve_season_from_media
STATUS_MAP: Dict[str, str] = {
"FINISHED": "finished",
"RELEASING": "ongoing",
"NOT_YET_RELEASED": "planned",
"CANCELLED": "planned",
"HIATUS": "ongoing",
}
SEASON_MAP: Dict[str, str] = {
"WINTER": "winter",
"SPRING": "spring",
"SUMMER": "summer",
"FALL": "fall",
}
def _title_names(media: Dict[str, Any]) -> Dict[str, List[str]]:
t = media.get("title") or {}
native = t.get("native")
english = t.get("english")
romaji = t.get("romaji")
res: Dict[str, List[str]] = {}
if native:
res.setdefault("ja", []).append(native)
if english:
res.setdefault("en", []).append(english)
if romaji:
res.setdefault("romaji", []).append(romaji)
return res
def _studio(media: Dict[str, Any]) -> Optional[Studio]:
studios_nodes = (media.get("studios") or {}).get("nodes") or []
if not studios_nodes:
return None
name = studios_nodes[0].get("name")
if not name:
return None
return Studio(id=None, name=name, poster=None, description=None)
def _tags(media: Dict[str, Any]) -> List[Tag]:
genres = media.get("genres") or []
res: List[Tag] = []
for g in genres:
if g:
res.append(Tag(names={"en": g}))
return res
def _poster(media: Dict[str, Any]) -> Optional[Image]:
cover = media.get("coverImage") or {}
url = cover.get("extraLarge") or cover.get("large")
if not url:
return None
return Image(id=None, storage_type=None, image_path=url)
def _status(media: Dict[str, Any]) -> Optional[str]:
raw = media.get("status")
if not raw:
return None
return STATUS_MAP.get(raw)
def _rating(media: Dict[str, Any]) -> Optional[float]:
avg = media.get("averageScore")
if avg is None:
return None
try:
return float(avg) / 10.0
except (TypeError, ValueError):
return None
def _rating_count(media: Dict[str, Any]) -> Optional[int]:
pop = media.get("popularity")
if pop is None:
return None
try:
return int(pop)
except (TypeError, ValueError):
return None
def _year_and_season(media: Dict[str, Any]) -> tuple[Optional[int], Optional[str]]:
year = media.get("seasonYear")
raw_season = media.get("season")
release_year = year if isinstance(year, int) else None
release_season = None
if isinstance(raw_season, str):
release_season = SEASON_MAP.get(raw_season.upper())
return release_year, release_season
def _episodes(media: Dict[str, Any]) -> tuple[Optional[int], Optional[int]]:
episodes_all = media.get("episodes")
if not isinstance(episodes_all, int):
episodes_all = None
next_ep = media.get("nextAiringEpisode") or {}
ep_num = next_ep.get("episode") if isinstance(next_ep, dict) else None
if not isinstance(ep_num, int):
ep_num = None
# базовая логика
if ep_num is not None:
episodes_aired = ep_num - 1
else:
episodes_aired = episodes_all
# приведение к инварианту БД:
# либо обе NULL, либо обе заданы и episodes_aired <= episodes_all
if episodes_aired is None and episodes_all is None:
return None, None
if episodes_all is None and episodes_aired is not None:
episodes_all = episodes_aired
if episodes_aired is None and episodes_all is not None:
episodes_aired = episodes_all
if (
episodes_aired is not None
and episodes_all is not None
and episodes_aired > episodes_all
):
episodes_aired = episodes_all
return episodes_aired, episodes_all
def _episodes_len(media: Dict[str, Any]) -> Optional[Dict[str, float]]:
duration = media.get("duration")
if duration is None:
return None
try:
return {"default": float(duration)}
except (TypeError, ValueError):
return None
def normalize_media(media: Dict[str, Any]) -> SourceTitle:
"""AniList Media JSON -> наш SourceTitle."""
title_names = _title_names(media)
studio = _studio(media)
tags = _tags(media)
poster = _poster(media)
title_status = _status(media)
rating = _rating(media)
rating_count = _rating_count(media)
release_year, release_season = _year_and_season(media)
episodes_aired, episodes_all = _episodes(media)
episodes_len = _episodes_len(media)
season = resolve_season_from_media(media)
return SourceTitle(
source=Source.ANILIST,
external_id=str(media["id"]),
title_names=title_names,
studio=studio,
tags=tags,
poster=poster,
title_status=title_status,
rating=rating,
rating_count=rating_count,
release_year=release_year,
release_season=release_season,
season=season,
episodes_aired=episodes_aired,
episodes_all=episodes_all,
episodes_len=episodes_len,
)

View file

@ -1,13 +0,0 @@
[project]
name = "anime-etl"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"aio-pika>=9.5.8",
"httpx>=0.28.1",
"psycopg[binary]>=3.3.1",
"pydantic>=2.12.5",
"python-dotenv>=1.2.1",
]

View file

@ -1,147 +0,0 @@
from __future__ import annotations
import asyncio
import json
import os
import sys
from typing import Any, Dict
import aio_pika
from aio_pika.exceptions import AMQPConnectionError
import psycopg
from psycopg.rows import dict_row
from services.anilist_importer import AniListImporter
PG_DSN = os.getenv("NYANIMEDB_PG_DSN") or os.getenv("DATABASE_URL")
RMQ_URL = os.getenv("NYANIMEDB_RMQ_URL") or os.getenv("RABBITMQ_URL") or "amqp://guest:guest@rabbitmq:5672/"
RPC_QUEUE_NAME = os.getenv("NYANIMEDB_IMPORT_RPC_QUEUE", "anime_import_rpc")
def rmq_request_to_filters(payload: Dict[str, Any]) -> Dict[str, Any]:
filters: Dict[str, Any] = {}
name = payload.get("name")
if isinstance(name, str) and name.strip():
filters["query"] = name.strip()
year = payload.get("year")
if isinstance(year, int) and year > 0:
filters["year"] = year
season = payload.get("season")
if isinstance(season, str) and season:
filters["season"] = season.lower()
filters.setdefault("limit", 10)
return filters
def create_handler(channel: aio_pika.Channel):
async def handle_message(message: aio_pika.IncomingMessage) -> None:
async with message.process():
try:
payload = json.loads(message.body.decode("utf-8"))
except json.JSONDecodeError:
return
if not isinstance(payload, dict):
return
filters = rmq_request_to_filters(payload)
timestamp = payload.get("timestamp")
try:
async with await psycopg.AsyncConnection.connect(
PG_DSN,
row_factory=dict_row,
) as conn:
importer = AniListImporter()
titles = await importer.import_by_filters_in_tx(conn, filters)
response: dict[str, Any] = {
"timestamp": timestamp,
"ok": True,
"titles": titles,
"error": None,
}
except Exception as e:
response = {
"timestamp": timestamp,
"ok": False,
"titles": [],
"error": {
"code": "import_failed",
"message": str(e),
},
}
body = json.dumps(response).encode("utf-8")
if message.reply_to:
await channel.default_exchange.publish(
aio_pika.Message(
body=body,
content_type="application/json",
correlation_id=message.correlation_id,
),
routing_key=message.reply_to,
)
return handle_message
async def connect_rmq_with_retry(
url: str,
retries: int = 20,
delay: float = 3.0,
) -> aio_pika.RobustConnection:
last_exc: Exception | None = None
for attempt in range(1, retries + 1):
try:
print(f"[worker] Connecting to RabbitMQ ({attempt}/{retries}) {url}", flush=True)
conn = await aio_pika.connect_robust(url)
print("[worker] Connected to RabbitMQ", flush=True)
return conn
except AMQPConnectionError as e:
last_exc = e
print(f"[worker] RabbitMQ connection failed: {e!r}, retry in {delay}s", flush=True)
await asyncio.sleep(delay)
print("[worker] Failed to connect to RabbitMQ after retries", file=sys.stderr, flush=True)
if last_exc:
raise last_exc
raise RuntimeError("Failed to connect to RabbitMQ")
async def main() -> None:
if not PG_DSN:
raise RuntimeError("PG_DSN is not set (NYANIMEDB_PG_DSN / DATABASE_URL)")
print(f"[worker] Starting. PG_DSN={PG_DSN!r}, RMQ_URL={RMQ_URL!r}, queue={RPC_QUEUE_NAME!r}", flush=True)
connection = await connect_rmq_with_retry(RMQ_URL)
channel = await connection.channel()
queue = await channel.declare_queue(
RPC_QUEUE_NAME,
durable=True,
)
handler = create_handler(channel)
await queue.consume(handler)
print(f"[*] Waiting for messages in '{RPC_QUEUE_NAME}'. Ctrl+C to exit.", flush=True)
try:
await asyncio.Future() # run forever
finally:
await connection.close()
if __name__ == "__main__":
if sys.platform.startswith("win"):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())

View file

@ -1,122 +0,0 @@
from __future__ import annotations
import asyncio
import time
from abc import ABC, abstractmethod
from typing import Awaitable, Callable, TypeVar, Optional
T = TypeVar("T")
NowFn = Callable[[], float]
SleepFn = Callable[[float], Awaitable[None]]
class RateLimiter(ABC):
"""
Базовый интерфейс rate-limiter'а.
"""
@abstractmethod
async def acquire(self) -> None:
"""
Дождаться слота для выполнения запроса.
"""
raise NotImplementedError
class TokenBucketRateLimiter(RateLimiter):
"""
Token-bucket rate limiter.
rate сколько токенов выдаём за период `per` секунд.
per длина окна в секундах.
capacity максимальное количество токенов в корзине (burst). По умолчанию = rate.
Пример:
limiter = TokenBucketRateLimiter(rate=30, per=60.0)
await limiter.acquire() # перед каждым запросом к AniList
"""
def __init__(
self,
rate: int,
per: float,
*,
capacity: Optional[int] = None,
now_fn: NowFn | None = None,
sleep_fn: SleepFn | None = None,
) -> None:
self.rate = float(rate)
self.per = float(per)
self.capacity = float(capacity if capacity is not None else rate)
self._now: NowFn = now_fn or time.monotonic
self._sleep: SleepFn = sleep_fn or asyncio.sleep
# начальное состояние: полный бак
self._tokens: float = self.capacity
self._updated_at: float = self._now()
self._lock = asyncio.Lock()
async def acquire(self) -> None:
"""
Подождать, пока не будет доступен хотя бы один токен.
Важно: ожидание (sleep) происходит ВНЕ lock'а, чтобы несколько корутин
могли "делить" ожидание.
"""
refill_rate_per_second = self.rate / self.per
while True:
async with self._lock:
now = self._now()
elapsed = now - self._updated_at
if elapsed > 0:
self._tokens = min(
self.capacity,
self._tokens + elapsed * refill_rate_per_second,
)
self._updated_at = now
if self._tokens >= 1.0:
self._tokens -= 1.0
return
# токенов нет — считаем, сколько нужно ждать
missing = 1.0 - self._tokens
wait_for = missing / refill_rate_per_second
if wait_for <= 0:
wait_for = 0.01
# спим уже без lock'а, чтобы другие могли проверить состояние
await self._sleep(wait_for)
def wrap_with_rate_limiter(
limiter: RateLimiter,
) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
"""
Декоратор для функций, которые делают запросы к внешнему API.
Пример:
limiter = TokenBucketRateLimiter(rate=30, per=60.0)
@wrap_with_rate_limiter(limiter)
async def fetch_something(...):
...
"""
def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
async def wrapper(*args, **kwargs) -> T:
await limiter.acquire()
return await func(*args, **kwargs)
return wrapper
return decorator
# Глобальные инстансы per-process для источников
ANILIST_RATE_LIMITER = TokenBucketRateLimiter(rate=30, per=60.0)
SHIKIMORI_RATE_LIMITER = TokenBucketRateLimiter(rate=90, per=60.0)
JIKAN_RATE_LIMITER = TokenBucketRateLimiter(rate=30, per=60.0)

View file

@ -1,93 +0,0 @@
# anime_etl/services/anilist_importer.py
from __future__ import annotations
from typing import Any, Dict, List
import psycopg
from psycopg.rows import dict_row
from sources.anilist_source import AniListSource
from canonicalizer import source_title_to_canonical
from db.repository import (
get_or_create_studio,
get_or_create_image,
insert_title_if_not_exists,
)
from models import CanonicalTitle
from jikan_studio_enricher import enrich_studio_with_jikan_desc
Conn = psycopg.AsyncConnection
class AniListImporter:
def __init__(self, source: AniListSource | None = None) -> None:
self._source = source or AniListSource()
async def import_by_filters_in_tx(
self,
conn: Conn,
filters: Dict[str, Any],
) -> List[Dict[str, Any]]:
"""
Выполнить импорт в рамках одной транзакции:
- поиск в AniList
- канонизация
- обогащение студии (Jikan)
- get_or_create_studio (+ illust_id)
- скачивание постера тайтла -> images
- insert_title_if_not_exists
"""
async with conn.transaction():
return await self._import_by_filters(conn, filters)
async def _import_by_filters(
self,
conn: Conn,
filters: Dict[str, Any],
) -> List[Dict[str, Any]]:
source_titles = await self._source.search(filters)
results: List[Dict[str, Any]] = []
for st in source_titles:
canonical: CanonicalTitle = source_title_to_canonical(st)
# 1) обогатить студию описанием из Jikan (если есть студия и ещё нет description)
if canonical.studio is None:
continue
canonical.studio = await enrich_studio_with_jikan_desc(canonical.studio)
# 2) создать/обновить студию (studio_name, illust_id, studio_desc)
studio_id = await get_or_create_studio(conn, canonical.studio)
# 3) скачать постер тайтла и создать запись в images
poster_id = await get_or_create_image(conn, canonical.poster, subdir="posters")
# 4) создать тайтл, если его ещё нет (с учётом studio_id и poster_id)
title_id = await insert_title_if_not_exists(conn, canonical, studio_id, poster_id)
results.append(
{
"id": title_id,
"title_names": canonical.title_names,
"release_year": canonical.release_year,
"release_season": canonical.release_season,
"season": canonical.season,
}
)
return results
async def import_from_anilist(
dsn: str,
filters: Dict[str, Any],
) -> List[Dict[str, Any]]:
"""
Открывает подключение к БД, делает транзакцию и импорт.
"""
importer = AniListImporter()
async with await psycopg.AsyncConnection.connect(dsn, row_factory=dict_row) as conn:
return await importer.import_by_filters_in_tx(conn, filters)

View file

@ -1,47 +0,0 @@
import httpx
import asyncio
from rate_limiter import ANILIST_RATE_LIMITER
URL = "https://graphql.anilist.co"
QUERY = """
query ($search: String, $season: MediaSeason, $seasonYear: Int, $format: MediaFormat, $page: Int, $perPage: Int) {
Page(page: $page, perPage: $perPage) {
media(search: $search, type: ANIME, season: $season, seasonYear: $seasonYear, format: $format) {
id
title { romaji english native }
status
season
seasonYear
format
episodes
duration
popularity
averageScore
coverImage { extraLarge large }
genres
studios(isMain: true) { nodes { id name } }
nextAiringEpisode { episode }
}
}
}
"""
CLIENT = httpx.AsyncClient(timeout=15.0)
async def _post(payload: dict) -> dict:
for i in range(5):
await ANILIST_RATE_LIMITER.acquire()
try:
r = await CLIENT.post(URL, json=payload)
r.raise_for_status()
return r.json()
except Exception:
await asyncio.sleep(1 * 2**i)
raise RuntimeError("AniList unreachable")
async def search_raw(filters: dict) -> list[dict]:
payload = {"query": QUERY, "variables": filters}
data = await _post(payload)
return data.get("data", {}).get("Page", {}).get("media") or []

View file

@ -1,35 +0,0 @@
from mappers.anilist_filters import to_anilist_filters
from sources.anilist_async_client import search_raw
from normalizers.anilist_normalizer import normalize_media
import asyncio
import pprint
class AniListSource:
async def search(self, local_filters: dict) -> list:
ani_filters = to_anilist_filters(local_filters)
raw_list = await search_raw(ani_filters)
return [normalize_media(r) for r in raw_list]
async def _demo() -> None:
src = AniListSource()
filters = {
"query": "monogatari",
# "year": 2017,
# "season": "winter",
# "type": "tv",
# "limit": 5,
}
print("Запускаю поиск с фильтрами:", filters)
titles = await src.search(filters)
print("Найдено тайтлов:", len(titles))
for t in titles:
# t.title_names — dict[str, list[str]]
# en = (t.title_names.get("en") or [""])[0]
# print("-", en, "|", t.release_year, t.release_season)
pprint.pprint(t)
if __name__ == "__main__":
asyncio.run(_demo())

View file

@ -1,54 +0,0 @@
import asyncio
from typing import Any, Dict, List, Optional
import httpx
from rate_limiter import JIKAN_RATE_LIMITER
BASE_URL = "https://api.jikan.moe/v4"
CLIENT = httpx.AsyncClient(timeout=15.0)
async def _get(path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Обёртка над GET с ретраями и rate-limit'ом.
"""
url = f"{BASE_URL}{path}"
last_exc: Exception | None = None
for i in range(5):
await JIKAN_RATE_LIMITER.acquire()
try:
r = await CLIENT.get(url, params=params)
r.raise_for_status()
return r.json()
except Exception as exc:
last_exc = exc
await asyncio.sleep(1 * 2**i)
raise RuntimeError(f"Jikan unreachable: {last_exc!r}")
async def search_producer(name: str, limit: int = 1) -> List[Dict[str, Any]]:
"""
Поиск студии/продюсера по имени.
Возвращает список элементов из data[]:
[{ "mal_id": int, "name": str, ... }, ...]
"""
if not name:
return []
data = await _get("/producers", {"q": name, "limit": limit})
return data.get("data") or []
async def fetch_producer_full(mal_id: int) -> Optional[Dict[str, Any]]:
"""
Полная инфа по producer'у (есть поле about).
"""
if not isinstance(mal_id, int):
return None
data = await _get(f"/producers/{mal_id}/full")
return data.get("data") or None

View file

@ -1,89 +0,0 @@
# anime_etl/utils/season_resolver.py
from __future__ import annotations
import re
from typing import Optional
_ROMAN = {
"I": 1, "II": 2, "III": 3, "IV": 4,
"V": 5, "VI": 6, "VII": 7, "VIII": 8,
"IX": 9, "X": 10,
}
def _roman_to_int(token: str) -> Optional[int]:
return _ROMAN.get(token.upper())
# Паттерны типа:
# - "Season 2"
# - "2nd Season"
# - "S3"
# - "III"
_SEASON_PATTERNS = [
# "Season 2"
re.compile(r"\bseason\s*(\d{1,2})\b", re.IGNORECASE),
# "2nd Season"
re.compile(r"\b(\d{1,2})(?:st|nd|rd|th)\s+season\b", re.IGNORECASE),
# "S3"
re.compile(r"\bs(\d{1,2})\b", re.IGNORECASE),
# одиночное число (осторожно, поэтому используем как самый последний fallback)
re.compile(r"\b(\d{1,2})\b"),
# римские цифры: "III"
re.compile(r"\b([IVX]{1,5})\b", re.IGNORECASE),
]
def extract_season_number_from_title(name: str) -> Optional[int]:
name = name.strip()
if not name:
return None
for pat in _SEASON_PATTERNS:
m = pat.search(name)
if not m:
continue
token = m.group(1)
# пробуем римские
roman = _roman_to_int(token)
if roman is not None:
return roman
# иначе просто число
try:
return int(token)
except ValueError:
continue
return None
def resolve_season_from_media(media: dict) -> Optional[int]:
"""
Определяем номер сезона по данным AniList Media.
Логика:
- Если формат не TV/ONA считаем, что это не нумерованный сезон (OVA/MOVIE/...)
- Берём title.english / romaji / native, пытаемся вытащить номер через regex.
"""
fmt = media.get("format")
if fmt not in ("TV", "ONA"):
return None
title = media.get("title") or {}
candidates: list[str] = []
for key in ("english", "romaji", "native"):
v = title.get(key)
if isinstance(v, str):
candidates.append(v)
for name in candidates:
n = extract_season_number_from_title(name)
if n is not None:
return n
return None

View file

@ -1,606 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "aio-pika"
version = "9.5.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiormq" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/73/8d1020683970de5532b3b01732d75c8bf922a6505fcdad1a9c7c6405242a/aio_pika-9.5.8.tar.gz", hash = "sha256:7c36874115f522bbe7486c46d8dd711a4dbedd67c4e8a8c47efe593d01862c62", size = 47408, upload-time = "2025-11-12T10:37:10.215Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/91/513971861d845d28160ecb205ae2cfaf618b16918a9cd4e0b832b5360ce7/aio_pika-9.5.8-py3-none-any.whl", hash = "sha256:f4c6cb8a6c5176d00f39fd7431e9702e638449bc6e86d1769ad7548b2a506a8d", size = 54397, upload-time = "2025-11-12T10:37:08.374Z" },
]
[[package]]
name = "aiormq"
version = "6.9.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pamqp" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/f6/01bc850db6d9b46ae825e3c373f610b0544e725a1159745a6de99ad0d9f1/aiormq-6.9.2.tar.gz", hash = "sha256:d051d46086079934d3a7157f4d8dcb856b77683c2a94aee9faa165efa6a785d3", size = 30554, upload-time = "2025-10-20T10:49:59.763Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/ec/763b13f148f3760c1562cedb593feaffbae177eeece61af5d0ace7b72a3e/aiormq-6.9.2-py3-none-any.whl", hash = "sha256:ab0f4e88e70f874b0ea344b3c41634d2484b5dc8b17cb6ae0ae7892a172ad003", size = 31829, upload-time = "2025-10-20T10:49:58.547Z" },
]
[[package]]
name = "anime-etl"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "aio-pika" },
{ name = "httpx" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pydantic" },
{ name = "python-dotenv" },
]
[package.metadata]
requires-dist = [
{ name = "aio-pika", specifier = ">=9.5.8" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.3.1" },
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
[[package]]
name = "certifi"
version = "2025.11.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "multidict"
version = "6.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" },
{ url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" },
{ url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" },
{ url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" },
{ url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" },
{ url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" },
{ url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" },
{ url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" },
{ url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" },
{ url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" },
{ url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" },
{ url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" },
{ url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" },
{ url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" },
{ url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" },
{ url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" },
{ url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" },
{ url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" },
{ url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" },
{ url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" },
{ url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" },
{ url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" },
{ url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" },
{ url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" },
{ url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" },
{ url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" },
{ url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" },
{ url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" },
{ url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" },
{ url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" },
{ url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" },
{ url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" },
{ url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" },
{ url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" },
{ url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" },
{ url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" },
{ url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" },
{ url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" },
{ url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" },
{ url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" },
{ url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" },
{ url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" },
{ url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" },
{ url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" },
{ url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" },
{ url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" },
{ url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" },
{ url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" },
{ url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" },
{ url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" },
{ url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" },
{ url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" },
{ url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" },
{ url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" },
{ url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" },
{ url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" },
{ url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" },
{ url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" },
{ url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" },
{ url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" },
{ url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" },
{ url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" },
{ url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" },
{ url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" },
{ url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" },
{ url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" },
{ url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" },
{ url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" },
{ url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" },
{ url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" },
{ url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" },
{ url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" },
{ url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" },
{ url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" },
{ url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" },
{ url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" },
{ url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" },
{ url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" },
{ url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" },
{ url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" },
{ url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" },
{ url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" },
{ url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" },
{ url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" },
{ url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" },
]
[[package]]
name = "pamqp"
version = "3.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" },
]
[[package]]
name = "propcache"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
{ url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
{ url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
{ url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
{ url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
{ url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
{ url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
{ url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
{ url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
{ url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
{ url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
{ url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
{ url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
{ url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
{ url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
{ url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
{ url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
{ url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
{ url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
{ url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
{ url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
{ url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
{ url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
{ url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
{ url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
{ url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
{ url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
{ url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
{ url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
{ url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
{ url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
{ url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
{ url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
{ url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
{ url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
{ url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
{ url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
{ url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
{ url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
{ url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
{ url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
{ url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
{ url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
{ url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
{ url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
{ url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
{ url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
{ url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
{ url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
{ url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
{ url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
{ url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
{ url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
{ url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
{ url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
{ url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
{ url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
{ url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
{ url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
{ url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
{ url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
{ url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
{ url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
{ url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
{ url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
{ url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
{ url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
{ url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
{ url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
]
[[package]]
name = "psycopg"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/ed/3a30e8ef82d4128c76aa9bd6b2a7fe6c16c283811e6655997f5047801b47/psycopg-3.3.1.tar.gz", hash = "sha256:ccfa30b75874eef809c0fbbb176554a2640cc1735a612accc2e2396a92442fc6", size = 165596, upload-time = "2025-12-02T21:09:55.545Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/f3/0b4a4c25a47c2d907afa97674287dab61bc9941c9ac3972a67100e33894d/psycopg-3.3.1-py3-none-any.whl", hash = "sha256:e44d8eae209752efe46318f36dd0fdf5863e928009338d736843bb1084f6435c", size = 212760, upload-time = "2025-12-02T21:02:36.029Z" },
]
[package.optional-dependencies]
binary = [
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
]
[[package]]
name = "psycopg-binary"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/29/12cfc28594aa940f5894da1b2f5368f9163260e3d6b53cf3eb9413d07489/psycopg_binary-3.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f0afb5653614ad4a9a2fa0fa8c593508a18bd319afc26b20a33b883f263bf90", size = 4579837, upload-time = "2025-12-02T21:07:09.264Z" },
{ url = "https://files.pythonhosted.org/packages/e7/f0/2b4cfca5161af8bb573963d9540f97b191a5dfe1afd02c3183feeade47a2/psycopg_binary-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b84ed483a4d0271be201005c7567161fc6bc884f7ebc08ed9f82083b3a0d1f9e", size = 4658787, upload-time = "2025-12-02T21:07:17.39Z" },
{ url = "https://files.pythonhosted.org/packages/e6/3b/ade0f141178633b098cc80af7922d13bbfc1014401232785af6e485563a2/psycopg_binary-3.3.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3323652b73305e23cc9b5f4e332b25f00c8cb16f47ef84ee4430b7df38273707", size = 5454896, upload-time = "2025-12-02T21:07:23.918Z" },
{ url = "https://files.pythonhosted.org/packages/65/14/90aac9ec57580da90bd6a0986288f0422b0a650f1686e10444b8b579c0f2/psycopg_binary-3.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c64ed9a49e606c764510a1d98270cc42a38527776aa98baf6e8c4e20c5341b96", size = 5132733, upload-time = "2025-12-02T21:07:28.789Z" },
{ url = "https://files.pythonhosted.org/packages/90/62/bb2da10712e409ec1579be67a879824ab484989de8ed773309c880b57213/psycopg_binary-3.3.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81eee7d7f5fa9a85778fb854d979ba16f48cec584c17c51117ba94ad9d6a667", size = 6724495, upload-time = "2025-12-02T21:07:34.821Z" },
{ url = "https://files.pythonhosted.org/packages/02/34/baf21418e62002c3cc0d35f4431b0b2953c44272e572ccd3b4161ffaa886/psycopg_binary-3.3.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8d0b967eada1831f6e8b652f6868c9fbdf80e397c1f096226fe0d545112f907d", size = 4964978, upload-time = "2025-12-02T21:07:39.34Z" },
{ url = "https://files.pythonhosted.org/packages/5a/b0/a375f37a852722878e2292f64dba8632d89c9afe0a3e0b9920a6bbcee847/psycopg_binary-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b2c86492e9b41d942bc263e5b961498bd404444b0547e1e2456e8f919599ad14", size = 4493649, upload-time = "2025-12-02T21:07:43.991Z" },
{ url = "https://files.pythonhosted.org/packages/bc/40/bb4bf3a141a1cbc36abd86867ca352c0807f062d5cb01d3e9141c685975b/psycopg_binary-3.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5468dcdb2717dc764d1e1d9a391b714d28717bc8613e2e5481f261718e4e72c5", size = 4173392, upload-time = "2025-12-02T21:07:48.509Z" },
{ url = "https://files.pythonhosted.org/packages/b1/e1/178274150e5f0398697e74c0027651c668ca2e2ec57db98c811ba97bf69b/psycopg_binary-3.3.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e3aa33e5553d12b91e23b928e869587289c6c26de58b3b14f70bed06eb767c58", size = 3909241, upload-time = "2025-12-02T21:07:52.85Z" },
{ url = "https://files.pythonhosted.org/packages/42/01/220119f18bf8447756b92f5db87a6e723ae1dd1db81ad591393714b71f5e/psycopg_binary-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bbd26acb1ba8416a16256bfd87de9a1427fb2e04f8d79eae3fb64a112ede06f1", size = 4219745, upload-time = "2025-12-02T21:07:57.374Z" },
{ url = "https://files.pythonhosted.org/packages/94/87/ece0da8b6befb17bb5ffd64eb28fb5ddd539d2569700f2e3e78e91385434/psycopg_binary-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ce74da70444348135f9b5b9b67121eb9816ef483159bf54083765792c948f249", size = 3537480, upload-time = "2025-12-02T21:08:01.029Z" },
{ url = "https://files.pythonhosted.org/packages/79/44/f907c508267bc203082217faf5750274f4c240471f99990db70ed9f4dada/psycopg_binary-3.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fda22ce8530236381ff79a674ebc319f1a224f2e39a44158774e55e1488f89b9", size = 4579070, upload-time = "2025-12-02T21:08:04.885Z" },
{ url = "https://files.pythonhosted.org/packages/08/61/554bd7b9b93aef79f0bd3c4d381d9809ecf38e55ab4eb5984f58a74695f7/psycopg_binary-3.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d2fc5fa6c45c406e43c8cc2787055d487b3ae597d2139125191b37fa04835f01", size = 4657517, upload-time = "2025-12-02T21:08:08.941Z" },
{ url = "https://files.pythonhosted.org/packages/b8/1b/c3bc20b72b9b056d1f7a3a131d676c66fe2bc7585f9d9f23d659d8725407/psycopg_binary-3.3.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4d97e4d27cc7ee6938faf7dc9919c452581b4795bd97f3f48582846f24ab81ed", size = 5452087, upload-time = "2025-12-02T21:08:15.144Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e6/58ae55874b963893f46d68748b28e05c432b0c109f53b40970ba6bf9fe95/psycopg_binary-3.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f4fb5a3cf4373a8b06c2feb8f29ff4c69968ba443687dedec9f79ce22ef339ae", size = 5131126, upload-time = "2025-12-02T21:08:23.463Z" },
{ url = "https://files.pythonhosted.org/packages/4c/28/bfcfb1b7c2907585292aaf61e902cbd00ecff50120178bc3e8268a74c1d6/psycopg_binary-3.3.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4de5298b81423648ae751c789a6adb7f9396bd4de7f402db8f469901d676ebe4", size = 6722913, upload-time = "2025-12-02T21:08:30.328Z" },
{ url = "https://files.pythonhosted.org/packages/c5/ba/7f6d3117059d4824854fb4eeafe7cb9fe069a43724f6a36b21aac0cd911d/psycopg_binary-3.3.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a41139976b8546b78ccd776b9f63665c247e209ae384fa6908ea401e9df2c385", size = 4966088, upload-time = "2025-12-02T21:08:34.666Z" },
{ url = "https://files.pythonhosted.org/packages/a5/18/d1baed589d7254be32eda44262787e397d1845fc7f08a30c38bf9345d361/psycopg_binary-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8ea017e58fa7fd8df1d9058ff0248e28f29312bf150a00114fc0ace8a800bfa", size = 4493332, upload-time = "2025-12-02T21:08:38.935Z" },
{ url = "https://files.pythonhosted.org/packages/08/62/57c6ac97cd4d1297115c3840fe09a19ae50b96294e3b8e988385497dc074/psycopg_binary-3.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:447e31350ea6816af03f39e6fc5ccccf18e34dc223a7d82734621048bb3fb9af", size = 4170782, upload-time = "2025-12-02T21:08:43.949Z" },
{ url = "https://files.pythonhosted.org/packages/12/27/b2577aad1baaa476cf482fb207850fb3f03467b3e6f3485e8cf360588ea8/psycopg_binary-3.3.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1f97f1814b046c8103b0a46a8c36c28399489716eba70ed38fbae30a27866e2c", size = 3910543, upload-time = "2025-12-02T21:08:48.202Z" },
{ url = "https://files.pythonhosted.org/packages/dc/89/7ad39e369706beda88827031504d9bb5fed58bf6fe50ec7045bd6750736b/psycopg_binary-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3986f783ff656a0392b81b7ac1e0f88a276a38276a5c93c0b89d4862e309d618", size = 4220070, upload-time = "2025-12-02T21:08:52.752Z" },
{ url = "https://files.pythonhosted.org/packages/ad/3a/703c155ab9fb64c874921116fd740c93494e6bd599b1bcd9e4987f5dcfb4/psycopg_binary-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:63e7c689a4249a1303da35df0b813e7cb3b9e2e5eae492a47482ee1a41dc66b2", size = 3540907, upload-time = "2025-12-02T21:08:56.898Z" },
{ url = "https://files.pythonhosted.org/packages/89/a5/056d227c85e4b769f2f9a2f2be71d1754492277410e15b2035637bafb92e/psycopg_binary-3.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7f7464571c2f4936810cdd7aed9108d3d80c6ea3d668a6e23fe8e9a4f4942d09", size = 4596368, upload-time = "2025-12-02T21:09:01.181Z" },
{ url = "https://files.pythonhosted.org/packages/eb/a8/5c628a984f3a53ec49d4f56827b6593abc609dc580f879e471fce39835b9/psycopg_binary-3.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4889aa16b2bfd4796a8648087262680d332fe9c6926fd7fd3d85c4f5eb483f01", size = 4675138, upload-time = "2025-12-02T21:09:06.095Z" },
{ url = "https://files.pythonhosted.org/packages/60/a8/e3cfd12e1bff144ce4af6e2c1ff72f2562a2853c4020843cb790c014c564/psycopg_binary-3.3.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:87a01fc62483b4cb5c343194d78ef9d7588624ad260fa82b31bf3c08e285a95f", size = 5456120, upload-time = "2025-12-02T21:09:11.385Z" },
{ url = "https://files.pythonhosted.org/packages/84/d7/70905001d6865c155b08badc5225d56daafbc064702cc42874f88d21bef8/psycopg_binary-3.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64b93d8456e17545c53bd23f601488fc508b68c4128133bc97762a00e61e8ab2", size = 5133485, upload-time = "2025-12-02T21:09:15.775Z" },
{ url = "https://files.pythonhosted.org/packages/49/6e/0cf90710f154d52163db920d059766ad27b510d90961a7f8f068ae3830f1/psycopg_binary-3.3.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f6b7bc0d230932aed188b9cc44b6fc6d43e2ec1585903d09a2d15095731ee07", size = 6731818, upload-time = "2025-12-02T21:09:25.092Z" },
{ url = "https://files.pythonhosted.org/packages/41/0f/c6c81861a8b2be54ff2b57a9eff84e50b3ec97d246dcb23311a25c9f78f0/psycopg_binary-3.3.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d4e3d94c2475fefcb7b93b507fe8fa5c8c61f993c9bece0ffd05906f1dbb47d", size = 4983866, upload-time = "2025-12-02T21:09:31.937Z" },
{ url = "https://files.pythonhosted.org/packages/83/88/9804e7749680f35d1eda8d9a979156f3f685a1af3a7c0f124b6a728e3836/psycopg_binary-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e0998da49b193d35641be04068f061965428a4b5e776065691b7f8c1bbc472e", size = 4516389, upload-time = "2025-12-02T21:09:36.058Z" },
{ url = "https://files.pythonhosted.org/packages/cc/cf/f136ba0afab5fce9c621883055918cee730671d22f83caa843dae0d728a8/psycopg_binary-3.3.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:cbf1c2fb03b7114808aa251ff6401cad0fa85e78dbecf45d711510797708b256", size = 4192382, upload-time = "2025-12-02T21:09:40.238Z" },
{ url = "https://files.pythonhosted.org/packages/ca/57/3c089efb7c52b455b3cfcc6d0a367e30f286777a3b291f93abe8eeeaa748/psycopg_binary-3.3.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:18c87c715dda836dcdf1f71c6b71a3d10194f07faf8358d2d42bc637d70137cf", size = 3928661, upload-time = "2025-12-02T21:09:44.658Z" },
{ url = "https://files.pythonhosted.org/packages/1c/95/a8096c8f61622ae74d55bc4442c5e86b009fe8a07e7e4dd58783fe14fc81/psycopg_binary-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ec0a04b95faf5c6c0af24917883b282f23dc588f0ee565efdd1146ed8129d258", size = 4239169, upload-time = "2025-12-02T21:09:49.4Z" },
{ url = "https://files.pythonhosted.org/packages/21/f0/9603f03eb2f887d47b6554def8f01317069515f4294878011b341759e332/psycopg_binary-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:c0bcb5a5ec01ccc34f884470473b2b9d1730513b7fb7175f741224af6af14182", size = 3642104, upload-time = "2025-12-02T21:09:53.514Z" },
]
[[package]]
name = "pydantic"
version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
[[package]]
name = "yarl"
version = "1.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
{ url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
{ url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
{ url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
{ url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
{ url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
{ url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
{ url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
{ url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
{ url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
{ url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
{ url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
{ url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
{ url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
{ url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
{ url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
{ url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
{ url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
{ url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
{ url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
{ url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
{ url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
{ url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
{ url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
{ url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
{ url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
{ url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
{ url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
{ url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
{ url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
{ url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
{ url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
{ url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
{ url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
{ url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
{ url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
{ url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
{ url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
{ url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
{ url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
{ url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
{ url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
{ url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
{ url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
{ url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
{ url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
{ url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
{ url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
{ url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
{ url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
{ url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
{ url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
{ url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
{ url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
{ url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
{ url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
{ url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
{ url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
{ url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
{ url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
{ url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
{ url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
{ url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
{ url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
{ url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
{ url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
{ url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
{ url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
{ url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
{ url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
{ url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
]

View file

@ -47,35 +47,28 @@ func CheckPassword(password, hash string) (bool, error) {
return argon2id.ComparePasswordAndHash(password, hash) return argon2id.ComparePasswordAndHash(password, hash)
} }
func (s *Server) generateImpersonationToken(userID string, impersonatedBy string) (string, error) { func (s Server) generateImpersonationToken(userID string, impersonated_by string) (accessToken string, err error) {
now := time.Now() accessClaims := jwt.MapClaims{
claims := auth.TokenClaims{ "user_id": userID,
ImpID: &impersonatedBy, "exp": time.Now().Add(15 * time.Minute).Unix(),
Type: "access", "imp_id": impersonated_by,
RegisteredClaims: jwt.RegisteredClaims{
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),
ID: generateJTI(),
},
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
return token.SignedString([]byte(s.JwtPrivateKey))
accessToken, err = at.SignedString([]byte(s.JwtPrivateKey))
if err != nil {
return "", err
}
return accessToken, nil
} }
func (s *Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) { func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) {
now := time.Now() accessClaims := jwt.MapClaims{
"user_id": userID,
// Access token (15 мин) "exp": time.Now().Add(15 * time.Minute).Unix(),
accessClaims := auth.TokenClaims{ //TODO: add created_at
Type: "access",
RegisteredClaims: jwt.RegisteredClaims{
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),
ID: generateJTI(),
},
} }
at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err = at.SignedString([]byte(s.JwtPrivateKey)) accessToken, err = at.SignedString([]byte(s.JwtPrivateKey))
@ -83,15 +76,9 @@ func (s *Server) generateTokens(userID string) (accessToken string, refreshToken
return "", "", "", err return "", "", "", err
} }
// Refresh token (7 дней) refreshClaims := jwt.MapClaims{
refreshClaims := auth.TokenClaims{ "user_id": userID,
Type: "refresh", "exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
RegisteredClaims: jwt.RegisteredClaims{
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(7 * 24 * time.Hour)),
ID: generateJTI(),
},
} }
rt := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) rt := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshToken, err = rt.SignedString([]byte(s.JwtPrivateKey)) refreshToken, err = rt.SignedString([]byte(s.JwtPrivateKey))
@ -99,7 +86,6 @@ func (s *Server) generateTokens(userID string) (accessToken string, refreshToken
return "", "", "", err return "", "", "", err
} }
// CSRF token
csrfBytes := make([]byte, 32) csrfBytes := make([]byte, 32)
_, err = rand.Read(csrfBytes) _, err = rand.Read(csrfBytes)
if err != nil { if err != nil {
@ -154,7 +140,7 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject
return auth.PostSignIn401Response{}, nil return auth.PostSignIn401Response{}, nil
} }
accessToken, refreshToken, csrfToken, err := s.generateTokens(fmt.Sprintf("%d", user.ID)) accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname)
if err != nil { if err != nil {
log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err) log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err)
// TODO: return 500 // TODO: return 500
@ -233,73 +219,56 @@ func (s Server) GetImpersonationToken(ctx context.Context, req auth.GetImpersona
return auth.GetImpersonationToken200JSONResponse{AccessToken: accessToken}, nil return auth.GetImpersonationToken200JSONResponse{AccessToken: accessToken}, nil
} }
func (s Server) RefreshTokens(ctx context.Context, req auth.RefreshTokensRequestObject) (auth.RefreshTokensResponseObject, error) { // func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) {
ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) // valid := false
if !ok { // var userID *string
log.Print("failed to get gin context") // var errStr *string
return auth.RefreshTokens500Response{}, fmt.Errorf("failed to get gin.Context from context.Context")
}
rtCookie, err := ginCtx.Request.Cookie("refresh_token") // token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) {
if err != nil { // if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
log.Print("failed to get refresh_token cookie") // return nil, fmt.Errorf("unexpected signing method")
return auth.RefreshTokens400Response{}, fmt.Errorf("failed to get refresh_token cookie") // }
} // return refreshSecret, nil
// })
refreshToken := rtCookie.Value // if err != nil {
// e := err.Error()
// errStr = &e
// return auth.PostAuthVerifyToken200JSONResponse{
// Valid: &valid,
// UserId: userID,
// Error: errStr,
// }, nil
// }
token, err := jwt.ParseWithClaims(refreshToken, &auth.TokenClaims{}, func(t *jwt.Token) (interface{}, error) { // if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { // if uid, ok := claims["user_id"].(string); ok {
return nil, fmt.Errorf("unexpected signing method") // // Refresh token is valid, generate new tokens
} // newAccessToken, newRefreshToken, _ := generateTokens(uid)
return []byte(s.JwtPrivateKey), nil // valid = true
}) // userID = &uid
if err != nil || !token.Valid { // return auth.PostAuthVerifyToken200JSONResponse{
log.Print("invalid refresh token") // Valid: &valid,
return auth.RefreshTokens401Response{}, nil // UserId: userID,
} // Error: nil,
// Token: &newAccessToken, // return new access token
// // optionally return newRefreshToken as well
// }, nil
// } else {
// e := "user_id not found in refresh token"
// errStr = &e
// }
// } else {
// e := "invalid refresh token claims"
// errStr = &e
// }
claims, ok := token.Claims.(*auth.TokenClaims) // return auth.PostAuthVerifyToken200JSONResponse{
if !ok || claims.Subject == "" { // Valid: &valid,
log.Print("invalid refresh token claims") // UserId: userID,
return auth.RefreshTokens401Response{}, nil // Error: errStr,
} // }, nil
if claims.Type != "refresh" { // }
log.Errorf("token is not a refresh token")
return auth.RefreshTokens401Response{}, nil
}
accessToken, refreshToken, csrfToken, err := s.generateTokens(claims.Subject)
if err != nil {
log.Errorf("failed to generate tokens for user %s: %v", claims.Subject, err)
return auth.RefreshTokens500Response{}, nil
}
// TODO: check cookie settings carefully
ginCtx.SetSameSite(http.SameSiteStrictMode)
ginCtx.SetCookie("access_token", accessToken, 900, "/api", "", false, true)
ginCtx.SetCookie("refresh_token", refreshToken, 1209600, "/auth", "", false, true)
ginCtx.SetCookie("xsrf_token", csrfToken, 1209600, "/", "", false, false)
return auth.RefreshTokens200Response{}, nil
}
func (s Server) Logout(ctx context.Context, req auth.LogoutRequestObject) (auth.LogoutResponseObject, error) {
// TODO: get current tokens and add them to block list
ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context)
if !ok {
log.Print("failed to get gin context")
return auth.Logout500Response{}, fmt.Errorf("failed to get gin.Context from context.Context")
}
// Delete cookies by setting MaxAge negative
// TODO: change secure to true
ginCtx.SetCookie("access_token", "", -1, "/api", "", false, true)
ginCtx.SetCookie("refresh_token", "", -1, "/auth", "", false, true)
ginCtx.SetCookie("xsrf_token", "", -1, "/", "", false, false)
return auth.Logout200Response{}, nil
}
func ExtractBearerToken(header string) (string, error) { func ExtractBearerToken(header string) (string, error) {
const prefix = "Bearer " const prefix = "Bearer "
@ -308,9 +277,3 @@ func ExtractBearerToken(header string) (string, error) {
} }
return header[len(prefix):], nil return header[len(prefix):], nil
} }
func generateJTI() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}

View file

@ -46,7 +46,7 @@ func main() {
log.Info("allow origins:", AppConfig.ServiceAddress) log.Info("allow origins:", AppConfig.ServiceAddress)
r.Use(cors.New(cors.Config{ r.Use(cors.New(cors.Config{
AllowOrigins: []string{AppConfig.ServiceAddress}, AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept"},
ExposeHeaders: []string{"Content-Length"}, ExposeHeaders: []string{"Content-Length"},

View file

@ -71,10 +71,7 @@ func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time {
func oapiDate2sql(t *time.Time) pgtype.Timestamptz { func oapiDate2sql(t *time.Time) pgtype.Timestamptz {
if t == nil { if t == nil {
return pgtype.Timestamptz{ return pgtype.Timestamptz{Valid: false}
Time: time.Now(),
Valid: true,
}
} }
return pgtype.Timestamptz{ return pgtype.Timestamptz{
Time: *t, Time: *t,
@ -170,7 +167,7 @@ func UserTitleStatus2Sqlc1(s *oapi.UserTitleStatus) (*sqlc.UsertitleStatusT, err
func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) { func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) {
oapi_usertitle := oapi.UserTitle{ oapi_usertitle := oapi.UserTitle{
Ftime: &t.UserFtime, Ctime: &t.UserCtime,
Rate: t.UserRate, Rate: t.UserRate,
ReviewId: t.ReviewID, ReviewId: t.ReviewID,
// Status: , // Status: ,
@ -389,9 +386,6 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque
// fmt.Println(pgErr.Code) // => 42601 // fmt.Println(pgErr.Code) // => 42601
if pgErr.Code == pgErrDuplicateKey { //duplicate key value if pgErr.Code == pgErrDuplicateKey { //duplicate key value
return oapi.AddUserTitle409Response{}, nil return oapi.AddUserTitle409Response{}, nil
} else {
log.Errorf("%v", err)
return oapi.AddUserTitle500Response{}, nil
} }
} else { } else {
log.Errorf("%v", err) log.Errorf("%v", err)
@ -404,7 +398,7 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque
return oapi.AddUserTitle500Response{}, nil return oapi.AddUserTitle500Response{}, nil
} }
oapi_usertitle := oapi.UserTitleMini{ oapi_usertitle := oapi.UserTitleMini{
Ftime: &user_title.Ftime, Ctime: &user_title.Ctime,
Rate: user_title.Rate, Rate: user_title.Rate,
ReviewId: user_title.ReviewID, ReviewId: user_title.ReviewID,
Status: oapi_status, Status: oapi_status,
@ -463,7 +457,7 @@ func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitl
} }
oapi_usertitle := oapi.UserTitleMini{ oapi_usertitle := oapi.UserTitleMini{
Ftime: &user_title.Ftime, Ctime: &user_title.Ctime,
Rate: user_title.Rate, Rate: user_title.Rate,
ReviewId: user_title.ReviewID, ReviewId: user_title.ReviewID,
Status: oapi_status, Status: oapi_status,
@ -493,7 +487,7 @@ func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleReque
return oapi.GetUserTitle500Response{}, nil return oapi.GetUserTitle500Response{}, nil
} }
oapi_usertitle := oapi.UserTitleMini{ oapi_usertitle := oapi.UserTitleMini{
Ftime: &user_title.Ftime, Ctime: &user_title.Ctime,
Rate: user_title.Rate, Rate: user_title.Rate,
ReviewId: user_title.ReviewID, ReviewId: user_title.ReviewID,
Status: oapi_status, Status: oapi_status,

View file

@ -3,11 +3,8 @@ package middleware
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"nyanimedb/auth"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
@ -40,18 +37,12 @@ func JWTAuthMiddleware(secret string) gin.HandlerFunc {
} }
// 2. Парсим токен с MapClaims // 2. Парсим токен с MapClaims
token, err := jwt.ParseWithClaims(tokenStr, &auth.TokenClaims{}, func(t *jwt.Token) (interface{}, error) { token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { if t.Method != jwt.SigningMethodHS256 {
return nil, fmt.Errorf("unexpected signing method") return nil, errors.New("unexpected signing method: " + t.Method.Alg())
} }
return []byte(secret), nil return []byte(secret), nil // ← конвертируем string → []byte
}) })
// token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
// if t.Method != jwt.SigningMethodHS256 {
// return nil, errors.New("unexpected signing method: " + t.Method.Alg())
// }
// return []byte(secret), nil // ← конвертируем string → []byte
// })
if err != nil { if err != nil {
abortWithJSON(c, http.StatusUnauthorized, "invalid token: "+err.Error()) abortWithJSON(c, http.StatusUnauthorized, "invalid token: "+err.Error())
return return
@ -64,23 +55,20 @@ func JWTAuthMiddleware(secret string) gin.HandlerFunc {
} }
// 4. Извлекаем user_id из claims // 4. Извлекаем user_id из claims
claims, ok := token.Claims.(*auth.TokenClaims) claims, ok := token.Claims.(jwt.MapClaims)
if !ok { if !ok {
abortWithJSON(c, http.StatusUnauthorized, "invalid claims format") abortWithJSON(c, http.StatusUnauthorized, "invalid claims format")
return return
} }
if claims.Subject == "" { userID, ok := claims["user_id"].(string)
if !ok || userID == "" {
abortWithJSON(c, http.StatusUnauthorized, "user_id claim missing or invalid") abortWithJSON(c, http.StatusUnauthorized, "user_id claim missing or invalid")
return return
} }
if claims.Type != "access" {
abortWithJSON(c, http.StatusUnauthorized, "token type is not access")
return
}
// 5. Сохраняем в контексте // 5. Сохраняем в контексте
c.Set("user_id", claims.Subject) c.Set("user_id", userID)
// 6. Для oapi-codegen — кладём gin.Context в request context // 6. Для oapi-codegen — кладём gin.Context в request context
GinContextToContext(c) GinContextToContext(c)

View file

@ -268,7 +268,7 @@ SELECT
u.status as usertitle_status, u.status as usertitle_status,
u.rate as user_rate, u.rate as user_rate,
u.review_id as review_id, u.review_id as review_id,
u.ftime as user_ftime, u.ctime as user_ctime,
i.storage_type as title_storage_type, i.storage_type as title_storage_type,
i.image_path as title_image_path, i.image_path as title_image_path,
COALESCE( COALESCE(
@ -370,7 +370,7 @@ WHERE
AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t)
GROUP BY GROUP BY
t.id, u.user_id, u.status, u.rate, u.review_id, u.ftime, i.id, s.id t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id
ORDER BY ORDER BY
CASE WHEN sqlc.arg('forward')::boolean THEN CASE WHEN sqlc.arg('forward')::boolean THEN
@ -400,7 +400,7 @@ FROM reviews
WHERE review_id = sqlc.arg('review_id')::bigint; WHERE review_id = sqlc.arg('review_id')::bigint;
-- name: InsertUserTitle :one -- name: InsertUserTitle :one
INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ftime) INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ctime)
VALUES ( VALUES (
sqlc.arg('user_id')::bigint, sqlc.arg('user_id')::bigint,
sqlc.arg('title_id')::bigint, sqlc.arg('title_id')::bigint,
@ -409,7 +409,7 @@ VALUES (
sqlc.narg('review_id')::bigint, sqlc.narg('review_id')::bigint,
sqlc.narg('ftime')::timestamptz sqlc.narg('ftime')::timestamptz
) )
RETURNING user_id, title_id, status, rate, review_id, ftime; RETURNING user_id, title_id, status, rate, review_id, ctime;
-- name: UpdateUserTitle :one -- name: UpdateUserTitle :one
-- Fails with sql.ErrNoRows if (user_id, title_id) not found -- Fails with sql.ErrNoRows if (user_id, title_id) not found
@ -417,7 +417,7 @@ UPDATE usertitles
SET SET
status = COALESCE(sqlc.narg('status')::usertitle_status_t, status), status = COALESCE(sqlc.narg('status')::usertitle_status_t, status),
rate = COALESCE(sqlc.narg('rate')::int, rate), rate = COALESCE(sqlc.narg('rate')::int, rate),
ftime = COALESCE(sqlc.narg('ftime')::timestamptz, ftime) ctime = COALESCE(sqlc.narg('ftime')::timestamptz, ctime)
WHERE WHERE
user_id = sqlc.arg('user_id') user_id = sqlc.arg('user_id')
AND title_id = sqlc.arg('title_id') AND title_id = sqlc.arg('title_id')

2
modules/bot/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
generated-client/
out/

View file

@ -0,0 +1,40 @@
cmake_minimum_required(VERSION 3.10.2)
project(AnimeBot)
set(SOURCES "")
file(GLOB_RECURSE SRC_FRONT "front/src/*.cpp")
list(APPEND SOURCES ${SRC_FRONT})
file(GLOB_RECURSE SRC_BACK "back/src/*.cpp")
list(APPEND SOURCES ${SRC_BACK})
file(GLOB_RECURSE SRC_API "generated-client/src/*.cpp")
list(APPEND SOURCES ${SRC_API})
file(GLOB_RECURSE SRC_AUTH "generated-client-auth/src/*.cpp")
list(APPEND SOURCES ${SRC_AUTH})
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(Boost_USE_MULTITHREADED ON)
set(CMAKE_BUILD_TYPE Debug)
find_package(Threads REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(Boost COMPONENTS system REQUIRED)
find_package(CURL)
find_library(CPPREST_LIB cpprest REQUIRED)
include_directories(/usr/local/include ${OPENSSL_INCLUDE_DIR} ${Boost_INCLUDE_DIR})
include_directories(front/include/)
include_directories(back/include)
include_directories(generated-client/include)
include_directories(generated-client-auth/include)
if (CURL_FOUND)
include_directories(${CURL_INCLUDE_DIRS})
add_definitions(-DHAVE_CURL)
endif()
add_executable(AnimeBot ${SOURCES})
target_link_libraries(AnimeBot /usr/local/lib/libTgBot.a ${CMAKE_THREAD_LIBS_INIT} ${OPENSSL_LIBRARIES} ${Boost_LIBRARIES} ${CURL_LIBRARIES} ${CPPREST_LIB})

1
modules/bot/README.md Normal file
View file

@ -0,0 +1 @@
### Здесь будет часть, отвечающая за телеграм ботика

View file

@ -0,0 +1,35 @@
// AuthImpersonationClient.hpp
#pragma once
#include <memory>
#include <string>
#include <stdexcept>
#include <cstdlib>
#include <map>
#include <cpprest/asyncrt_utils.h>
#include "AuthClient/ApiClient.h"
#include "AuthClient/ApiConfiguration.h"
#include "AuthClient/api/AuthApi.h"
#include "AuthClient/model/GetImpersonationToken_request.h"
#include "AuthClient/model/GetImpersonationToken_200_response.h"
namespace nyanimed {
class AuthImpersonationClient {
public:
AuthImpersonationClient();
// Потокобезопасный вызов — не модифицирует состояние
pplx::task<std::shared_ptr<nyanimed::meow::auth::model::GetImpersonationToken_200_response>>
getImpersonationToken(int64_t userId) const;
private:
std::string m_baseUrl;
std::string m_authToken;
std::shared_ptr<nyanimed::meow::auth::api::ApiClient> m_apiClient;
std::shared_ptr<nyanimed::meow::auth::api::AuthApi> m_authApi;
};
} // namespace nyanimed

View file

@ -0,0 +1,39 @@
#pragma once
#include "CppRestOpenAPIClient/ApiClient.h"
#include "CppRestOpenAPIClient/ApiConfiguration.h"
#include "CppRestOpenAPIClient/api/DefaultApi.h"
#include "CppRestOpenAPIClient/model/User.h"
#include "CppRestOpenAPIClient/model/GetUserTitles_200_response.h"
#include "CppRestOpenAPIClient/model/UserTitle.h"
#include "CppRestOpenAPIClient/model/Title.h"
#include "constants.hpp"
#include "structs.hpp"
#include <iostream>
#include <thread>
#include <functional>
#include <memory>
#include <cpprest/asyncrt_utils.h>
#include <boost/optional.hpp>
#include "AuthImpersonation.hpp"
using namespace org::openapitools::client::api;
class BotToServer {
public:
BotToServer();
// Асинхронный метод: получить список тайтлов пользователя
pplx::task<std::vector<BotStructs::Title>> fetchUserTitlesAsync(const std::string& userId);
pplx::task<BotStructs::Title> fetchTitleAsync(const std::string& userId, int64_t titleId);
private:
std::shared_ptr<org::openapitools::client::api::ApiConfiguration> apiconfiguration;
std::shared_ptr<org::openapitools::client::api::ApiClient> apiclient;
std::shared_ptr<org::openapitools::client::api::DefaultApi> api;
nyanimed::AuthImpersonationClient authClient;
static BotStructs::Title mapTitleToBotTitle(
const std::shared_ptr<org::openapitools::client::model::Title>& titleModel);
};

View file

@ -0,0 +1,34 @@
#include "AuthImpersonation.hpp"
nyanimed::AuthImpersonationClient::AuthImpersonationClient() {
const char* baseUrlEnv = std::getenv("NYANIMEDBAUTHURL");
const char* tokenEnv = std::getenv("NYANIMEDBAUTHTOKEN");
if (!baseUrlEnv || std::string(baseUrlEnv).empty()) {
throw std::runtime_error("Missing required environment variable: NYANIMEDBAUTHURL");
}
if (!tokenEnv || std::string(tokenEnv).empty()) {
throw std::runtime_error("Missing required environment variable: NYANIMEDBAUTHTOKEN");
}
m_baseUrl = baseUrlEnv;
m_authToken = tokenEnv;
auto config = std::make_shared<nyanimed::meow::auth::api::ApiConfiguration>();
config->setBaseUrl(utility::conversions::to_string_t(m_baseUrl));
m_apiClient = std::make_shared<nyanimed::meow::auth::api::ApiClient>(config);
m_authApi = std::make_shared<nyanimed::meow::auth::api::AuthApi>(m_apiClient);
}
pplx::task<std::shared_ptr<nyanimed::meow::auth::model::GetImpersonationToken_200_response>>
nyanimed::AuthImpersonationClient::getImpersonationToken(int64_t userId) const {
auto request = std::make_shared<nyanimed::meow::auth::model::GetImpersonationToken_request>();
request->setUserId(userId);
//request->setExternalId(externalId);
std::map<utility::string_t, utility::string_t> headers;
headers[U("Authorization")] = U("Bearer ") + utility::conversions::to_string_t(m_authToken);
return m_authApi->getImpersonationToken(request, headers);
}

View file

@ -0,0 +1,216 @@
#include "BotToServer.hpp"
BotToServer::BotToServer() {
apiconfiguration = std::make_shared<ApiConfiguration>();
const char* envUrl = getenv("NYANIMEDBBASEURL");
if (!envUrl) {
std::runtime_error("Environment variable NYANIMEDBBASEURL is not set");
}
apiconfiguration->setBaseUrl(utility::conversions::to_string_t(envUrl));
apiconfiguration->setUserAgent(utility::conversions::to_string_t("OpenAPI Client"));
apiclient = std::make_shared<ApiClient>(apiconfiguration);
api = std::make_shared<DefaultApi>(apiclient);
}
// Вспомогательная функция: преобразует UserTitle → BotStructs::Title
static BotStructs::Title mapUserTitleToBotTitle(
const std::shared_ptr<org::openapitools::client::model::UserTitle>& userTitle
) {
if (!userTitle || !userTitle->titleIsSet()) {
return BotStructs::Title{0, "Invalid", "", -1};
}
auto apiTitle = userTitle->getTitle();
if (!apiTitle) {
return BotStructs::Title{0, "No Title", "", -1};
}
int64_t id = apiTitle->getId();
std::string name = "Untitled";
auto titleNames = apiTitle->getTitleNames();
utility::string_t ru = U("ru");
if (titleNames.find(ru) != titleNames.end() && !titleNames.at(ru).empty()) {
name = utility::conversions::to_utf8string(titleNames.at(ru).front());
} else if (!titleNames.empty()) {
const auto& firstLang = *titleNames.begin();
if (!firstLang.second.empty()) {
name = utility::conversions::to_utf8string(firstLang.second.front());
}
}
std::string description = ""; // описание пока не поддерживается в OpenAPI-модели
return BotStructs::Title{id, name, description, -1};
}
pplx::task<std::vector<BotStructs::Title>> BotToServer::fetchUserTitlesAsync(const std::string& userId) {
// Шаг 1: Получаем impersonation-токен
auto impersonationTask = authClient.getImpersonationToken(std::stoi(userId));
// Шаг 2: После получения токена — делаем запрос getUserTitles с этим токеном
return impersonationTask.then([=](pplx::task<std::shared_ptr<nyanimed::meow::auth::model::GetImpersonationToken_200_response>> tokenTask) {
try {
auto tokenResponse = tokenTask.get();
if (!tokenResponse) {
throw std::runtime_error("Null response from getImpersonationToken");
}
utility::string_t accessToken = utility::conversions::to_string_t(tokenResponse->getAccessToken());
// Формируем заголовки с токеном
std::map<utility::string_t, utility::string_t> customHeaders;
customHeaders[U("Cookie")] = U("access_token=") + accessToken;
// Подготавливаем параметры запроса
utility::string_t userIdW = utility::conversions::to_string_t(userId);
int32_t limit = static_cast<int32_t>(BotConstants::DISP_TITLES_NUM);
// Шаг 3: Выполняем getUserTitles с кастомными заголовками
return api->getUserTitles(
userIdW,
boost::none, // cursor
boost::none, // sort
boost::none, // sortForward
boost::none, // word
boost::none, // status
boost::none, // watchStatus
boost::none, // rating
boost::none, // myRate
boost::none, // releaseYear
boost::none, // releaseSeason
limit,
boost::none, // fields
customHeaders
);
} catch (const std::exception& e) {
std::cerr << "Error obtaining impersonation token: " << e.what() << std::endl;
throw; // Пробрасываем, чтобы цепочка task.then завершилась с ошибкой
}
}).then([=](pplx::task<std::shared_ptr<org::openapitools::client::model::GetUserTitles_200_response>> responseTask) {
try {
auto response = responseTask.get();
if (!response) {
throw std::runtime_error("Null response from getUserTitles");
}
const auto& userTitles = response->getData();
std::vector<BotStructs::Title> result;
result.reserve(userTitles.size());
for (size_t i = 0; i < userTitles.size(); ++i) {
BotStructs::Title botTitle = mapUserTitleToBotTitle(userTitles[i]);
botTitle.num = static_cast<int64_t>(i); // 0-based индекс
result.push_back(botTitle);
}
return result;
} catch (const web::http::http_exception& e) {
std::cerr << "HTTP error in fetchUserTitlesAsync: " << e.what() << std::endl;
throw;
} catch (const std::exception& e) {
std::cerr << "Error in fetchUserTitlesAsync: " << e.what() << std::endl;
throw;
}
});
}
pplx::task<BotStructs::Title> BotToServer::fetchTitleAsync(const std::string& userId, int64_t titleId) {
auto impersonationTask = authClient.getImpersonationToken(std::stoi(userId));
// Шаг 2: После получения токена — делаем запрос getTitle с этим токеном
return impersonationTask.then([=](pplx::task<std::shared_ptr<nyanimed::meow::auth::model::GetImpersonationToken_200_response>> tokenTask) {
try {
auto tokenResponse = tokenTask.get();
if (!tokenResponse) {
throw std::runtime_error("Null response from getImpersonationToken");
}
utility::string_t accessToken = utility::conversions::to_string_t(tokenResponse->getAccessToken());
// Формируем заголовки с токеном
std::map<utility::string_t, utility::string_t> customHeaders;
customHeaders[U("Cookie")] = U("access_token=") + accessToken;
// Шаг 3: Выполняем запрос getTitle
return api->getTitle(
titleId,
boost::none, // fields — оставляем по умолчанию (все поля)
customHeaders
);
} catch (const std::exception& e) {
std::cerr << "Error obtaining impersonation token in fetchTitleAsync: " << e.what() << std::endl;
throw;
}
}).then([](pplx::task<std::shared_ptr<org::openapitools::client::model::Title>> responseTask) {
try {
auto response = responseTask.get();
if (!response) {
throw std::runtime_error("Null response from getTitle");
}
// Преобразуем модель OpenAPI в внутреннюю структуру бота
BotStructs::Title botTitle = mapTitleToBotTitle(response);
return botTitle;
} catch (const web::http::http_exception& e) {
std::cerr << "HTTP error in fetchTitleAsync: " << e.what() << std::endl;
throw;
} catch (const std::exception& e) {
std::cerr << "Error in fetchTitleAsync: " << e.what() << std::endl;
throw;
}
});
}
BotStructs::Title BotToServer::mapTitleToBotTitle(
const std::shared_ptr<org::openapitools::client::model::Title>& titleModel)
{
if (!titleModel) {
throw std::invalid_argument("titleModel is null");
}
BotStructs::Title botTitle;
botTitle.id = titleModel->getId();
botTitle.num = 0;
botTitle.description = ""; // Описание недоступно в текущей модели
// Извлекаем название
std::string titleName;
const auto& titleNames = titleModel->getTitleNames();
// Попробуем ru → en → первый попавшийся
std::vector<utility::string_t> preferredLangs = { U("ru"), U("en") };
bool found = false;
for (const auto& lang : preferredLangs) {
auto it = titleNames.find(lang);
if (it != titleNames.end() && !it->second.empty()) {
titleName = utility::conversions::to_utf8string(it->second[0]);
found = true;
break;
}
}
if (!found && !titleNames.empty()) {
// Берём первый язык и первое название
const auto& firstLang = *titleNames.begin();
if (!firstLang.second.empty()) {
titleName = utility::conversions::to_utf8string(firstLang.second[0]);
}
}
botTitle.name = titleName;
// --- Изображение ---
botTitle.imageUrl = "";
if (titleModel->posterIsSet()) {
auto poster = titleModel->getPoster();
if (poster && poster->imagePathIsSet()) {
botTitle.imageUrl = utility::conversions::to_utf8string(poster->getImagePath());
}
}
return botTitle;
}

2
modules/bot/front/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
build/
out/

View file

@ -0,0 +1,63 @@
#pragma once
#include <unordered_map>
#include <vector>
#include <mutex>
#include <optional>
#include "constants.hpp"
enum class UserState {
MAIN_MENU, // Главное меню
VIEWING_MY_TITLES, // Список моих тайтлов
AWAITING_TITLE_NAME, // Жду название тайтла для поиска
VIEWING_FOUND_TITLES, // Смотрю найденные тайтлы
VIEWING_TITLE_PAGE, // Смотрю страничку тайтла
AWAITING_REVIEW, // Жду ревью на тайтл
VIEWING_REVIEW_LIST, // Смотрю список ревью на тайтл
VIEWING_REVIEW, // Смотрю (конкретное) ревью на тайтл
VIEWING_DESCRIPTION, // Смотрю описание тайтла
ERROR, // Ошибка состояния
};
struct NavigationStep {
UserState state;
int64_t payload; // ID тайтла, ревью и т.д.
};
struct UserContext {
int64_t userId;
std::vector<NavigationStep> history; // Текущее состояние пользователя + история предыдущих состояний
};
class BotUserContext {
private:
mutable std::mutex mtx;
std::unordered_map<int64_t, UserContext> userContexts;
public:
// Получить копию контекста пользователя (или std::nullopt, если не найден)
std::optional<UserContext> getContext(int64_t userId) const;
// Установить/обновить контекст пользователя
void setContext(int64_t userId, const UserContext& context);
// Добавить шаг навигации к существующему контексту пользователя
// Если пользователя нет — создаётся новый контекст
void pushNavigationStep(int64_t userId, const NavigationStep& step);
// Заменить текущую историю (полезно, например, при сбросе состояния)
void setNavigationHistory(int64_t userId, const std::vector<NavigationStep>& history);
// Получить текущий шаг (последний в истории) или std::nullopt, если нет истории
std::optional<NavigationStep> getCurrentStep(int64_t userId) const;
// pop последнего состояния. true в случае удачи
bool popStep(int64_t userId);
// Удалить контекст пользователя (например, при логауте)
void removeContext(int64_t userId);
/// @brief Создает контекст начального меню для пользователя
/// @param userId
void createInitContext(int64_t userId);
};

View file

@ -0,0 +1,17 @@
#include <tgbot/tgbot.h>
#include "structs.hpp"
class KeyboardFactory {
public:
/// Create keyboard for main menu
static TgBot::InlineKeyboardMarkup::Ptr createMainMenu();
/// Create keyboard for My_Titles
static TgBot::InlineKeyboardMarkup::Ptr createMyTitles(std::vector<BotStructs::Title> titles);
/// Create keyboard for sendError
static TgBot::InlineKeyboardMarkup::Ptr createError(const std::string& errorCallback);
/// Create keyboard for Title page
static TgBot::InlineKeyboardMarkup::Ptr createTitleMenu(int64_t title_id);
};

View file

@ -0,0 +1,44 @@
#pragma once
#include <string>
namespace BotConstants {
const int64_t NULL_PAYLOAD = -1; // Default value для payload
const int64_t DISP_TITLES_NUM = 6; // Количество тайтлов, отображаемых на страничке
const int64_t DISP_REVIEW_NUM = 4; // Количество ревью, отображаемых на страничке
namespace Button {
const std::string TO_MAIN_MENU = "Главное меню";
const std::string FIND_ANIME = "Найти аниме";
const std::string MY_TITLES = "Мои тайтлы";
const std::string PREV = "<<Назад";
const std::string NEXT = "Дальше>>";
}
namespace Callback {
const std::string ACTION = "action:";
const std::string FIND_ANIME = ACTION + "find_anime";
const std::string ADD_REVIEW = ACTION + "add_review";
const std::string ADD_STATUS = ACTION + "add_status";
const std::string STATUS = "status:";
const std::string WATCHING = STATUS + "watching";
const std::string SEEN = STATUS + "seen";
const std::string WANT = STATUS + "want";
const std::string THROWN = STATUS + "thrown";
const std::string NAVIGATION = "navigation:";
const std::string MAIN_MENU = NAVIGATION + "main_menu";
const std::string MY_TITLES = NAVIGATION + "my_titles";
const std::string LIST_PREV = NAVIGATION + "prev"; // Пагинация
const std::string LIST_NEXT = NAVIGATION + "next"; // Пагинация
const std::string NAV_BACK = NAVIGATION + "back"; // Возврат по стеку состояний
const std::string CHOICE = "choice:";
const std::string ERROR = "error:";
const std::string ERROR_NAVIGATION = ERROR + NAVIGATION;
const std::string ERROR_AUTH = ERROR + "auth";
}
namespace Text {
const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?";
const std::string SAD_ERROR = "У нас что-то случилось:(\nМы обязательно скоро исправимся";
const std::string AUTH_ERROR = "Проблемы с авторизацией, попробуйте авторизоваться повторно";
const std::string SERVER_ERROR = "Не удалось загрузить данные. Попробуйте позже.";
}
}

View file

@ -0,0 +1,27 @@
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <string>
#include <tgbot/tgbot.h>
#include "handlers.hpp"
class AnimeBot {
private:
TgBot::Bot bot;
BotHandlers handler;
public:
/// Init Bot
AnimeBot(const std::string& token);
/// Main menu
void setupHandlers();
/// Get TgBot::Bot
TgBot::Bot& getBot();
/// Function creates main menu and sends it to user with chatId
void sendMainMenu(int64_t chatId);
};

View file

@ -0,0 +1,112 @@
#pragma once
#include <tgbot/tgbot.h>
#include <string>
#include <structs.hpp>
#include <unordered_map>
#include <string>
#include <stdexcept>
#include <cctype>
#include "BotToServer.hpp"
#include "BotUserContext.hpp"
/// @brief Структура возвращаемого значения класса BotHandlers для изменения текущего сообщения
struct HandlerResult {
std::string message;
TgBot::InlineKeyboardMarkup::Ptr keyboard;
};
class BotHandlers {
public:
BotHandlers(TgBot::Api api) : botApi(api) {;}
/// @brief Обработка callback'ов из кнопок интерфейса
/// @param query запрос callback
void handleCallback(TgBot::CallbackQuery::Ptr query);
/// @brief Обработка сообщений боту
/// @details
/// Функция для обработки сообщений, которые юзер отправляет
/// боту. Необходима для обработки ревью и названий искомого
/// аниме. Внутри себя проверяет текущий state пользователя
/// в боте.
/// @param message обрабатываемое сообщение
void handleMessage(TgBot::Message::Ptr message);
void initUser(int64_t userId);
private:
TgBot::Api botApi;
BotUserContext contextManager;
BotToServer server_;
void handleNavigation(TgBot::CallbackQuery::Ptr query);
void handleError(TgBot::CallbackQuery::Ptr query);
void processCallbackImpl(TgBot::CallbackQuery::Ptr query);
/// @brief Получить очередную страницу тайтлов из списка пользователя
/// @param userId Идентификатор пользователя
/// @param payload Полезная нагрузка
/// @return HandlerResult
/// static HandlerResult returnMyTitles(int64_t userId, int64_t payload);
/// @brief Уменьшает значение нагрузки с учетом текущего состояния
/// @param payload Изменяемое значение нагрузки
/// @param curState Текущее состояние
void reducePayload(int64_t& payload, const UserState curState);
/// @brief Увеличивает значение нагрузки с учетом текущего состояния
/// @param payload Изменяемое значение нагрузки
/// @param curState Текущее состояние
void increasePayload(int64_t& payload, const UserState curState);
/// @brief Редактирует текущее сообщение в диалоге с пользователем
/// @details Меняет текст сообщения и клавиатуру на те, что передаются
/// в аргументе response. Информацию об id чата и изменяемого сообщения
/// забирает из query, который возвращается с callback'ом после нажатия
/// кнопки в интерфейсе
/// @param query Callback запрос
/// @param response Параметры ответа: клавиатура и текст
void editMessage(int64_t chatId, int64_t messageId, HandlerResult response);
/// @brief Редактирует текущее сообщение в диалоге с пользователем
/// @details Меняет текст сообщения и клавиатуру на те, что передаются
/// в аргументе response, а также прикрепляет картинку
/// @param chatId id чата
/// @param messageId id меняемого сообщения
/// @param imageUrl ссылка на вставляемую картинку (должна быть доступна в публичной сети)
/// @param response Параметры ответа: клавиатура и текст
void editMessageWithPhoto(
int64_t chatId,
int64_t messageId,
const std::string& imageUrl,
HandlerResult response);
/// @brief Отрисовка текущего экрана (соотв. контексту)
/// @param ctx - текущий контекст
void renderCurrent(TgBot::CallbackQuery::Ptr query);
/// @brief Логика переходов между контекстами (навигация на следующий шаг)
/// @param query - запрос
/// @param current - текущий шаг навигации
/// @return следующий NavigationStep при успехе (std::nullopt в случае ошибки)
std::optional<NavigationStep> computeNextStep(const TgBot::CallbackQuery::Ptr& query,
const NavigationStep& current);
/// @brief Получить состояние страницы главного меню
/// @return HandlerResult с параметрами главного меню
HandlerResult showMainMenu();
/// @brief Посылает интерфейс обработки ошибки на callback запрос
/// @param query запрос
void sendError(int64_t chatId, int64_t messageId, const std::string& errText);
// Форматирование для отображения в сообщении
std::string formatTitlesList(const std::vector<BotStructs::Title>& titles);
// Форматирование сообщения на страничке с тайтлом
std::string formatTitle(const BotStructs::Title& title);
// Парсинг id тайтла из callback'а
int64_t parseId(const std::string& data);
};

View file

@ -0,0 +1,11 @@
#pragma once
namespace BotStructs {
struct Title {
int64_t id;
std::string name;
std::string description;
int64_t num;
std::string imageUrl;
};
}

View file

@ -0,0 +1,58 @@
#include "BotUserContext.hpp"
std::optional<UserContext> BotUserContext::getContext(int64_t userId) const {
std::lock_guard<std::mutex> lock(mtx);
auto it = userContexts.find(userId);
if (it != userContexts.end()) {
return it->second;
}
return std::nullopt;
}
void BotUserContext::setContext(int64_t userId, const UserContext& context) {
std::lock_guard<std::mutex> lock(mtx);
userContexts[userId] = context;
}
void BotUserContext::pushNavigationStep(int64_t userId, const NavigationStep& step) {
std::lock_guard<std::mutex> lock(mtx);
auto& ctx = userContexts[userId];
ctx.userId = userId;
ctx.history.push_back(step);
}
void BotUserContext::setNavigationHistory(int64_t userId, const std::vector<NavigationStep>& history) {
std::lock_guard<std::mutex> lock(mtx);
auto& ctx = userContexts[userId];
ctx.userId = userId;
ctx.history = history;
}
std::optional<NavigationStep> BotUserContext::getCurrentStep(int64_t userId) const {
std::lock_guard<std::mutex> lock(mtx);
auto it = userContexts.find(userId);
if (it != userContexts.end() && !it->second.history.empty()) {
return it->second.history.back();
}
return std::nullopt;
}
void BotUserContext::removeContext(int64_t userId) {
std::lock_guard<std::mutex> lock(mtx);
userContexts.erase(userId);
}
bool BotUserContext::popStep(int64_t userId) {
std::lock_guard<std::mutex> lock(mtx);
auto it = userContexts.find(userId);
if (it != userContexts.end() && (it->second.history.size() > 1)) {
it->second.history.pop_back();
return true;
}
return false;
}
void BotUserContext::createInitContext(int64_t userId) {
NavigationStep initStep = {UserState::MAIN_MENU, BotConstants::NULL_PAYLOAD};
setContext(userId, {userId, {initStep}});
}

View file

@ -0,0 +1,83 @@
#include "KeyboardFactory.hpp"
#include "constants.hpp"
TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMainMenu() {
auto keyboard = std::make_shared<TgBot::InlineKeyboardMarkup>();
TgBot::InlineKeyboardButton::Ptr button1(new TgBot::InlineKeyboardButton);
button1->text = BotConstants::Button::FIND_ANIME;
button1->callbackData = BotConstants::Callback::FIND_ANIME;
TgBot::InlineKeyboardButton::Ptr button2(new TgBot::InlineKeyboardButton);
button2->text = BotConstants::Button::MY_TITLES;
button2->callbackData = BotConstants::Callback::MY_TITLES;
keyboard->inlineKeyboard = {{button1, button2}};
return keyboard;
}
// TODO: Переписать с учетом констант на количество отображаемых тайтлов и нового callback'a
TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<BotStructs::Title> titles) {
auto keyboard = std::make_shared<TgBot::InlineKeyboardMarkup>();
std::vector<TgBot::InlineKeyboardButton::Ptr> row;
std::vector<std::vector<TgBot::InlineKeyboardButton::Ptr>> layout;
int counter = 0;
for(BotStructs::Title& title : titles) {
if(counter >= BotConstants::DISP_TITLES_NUM) {
break;
}
auto button = std::make_shared<TgBot::InlineKeyboardButton>();
button->text = std::to_string(title.num + 1) + " " + title.name;
button->callbackData = BotConstants::Callback::CHOICE + std::to_string(title.num);
row.push_back(button);
counter++;
if(counter % 2 == 0) {
layout.push_back(row);
row.clear();
}
}
if (!row.empty()) {
layout.push_back(row);
row.clear();
}
// TODO: Додумать логику, когда пришло 6 записей в конце
if(counter % 2 == 1) {
auto button = std::make_shared<TgBot::InlineKeyboardButton>();
button->text = BotConstants::Button::PREV;
if(titles[0].num == 0) {
button->callbackData = BotConstants::Callback::NAV_BACK;
}
else {
button->callbackData = BotConstants::Callback::LIST_PREV + ':' + std::to_string(titles[0].num);
}
layout.back().push_back(button);
}
else {
auto button_prev = std::make_shared<TgBot::InlineKeyboardButton>();
button_prev->text = BotConstants::Button::PREV;
if(titles[0].num == 0) {
button_prev->callbackData = BotConstants::Callback::NAV_BACK;
}
else {
button_prev->callbackData = BotConstants::Callback::LIST_PREV + ':' + std::to_string(titles[0].num);
}
auto button_next = std::make_shared<TgBot::InlineKeyboardButton>();
button_next->text = BotConstants::Button::NEXT;
button_next->callbackData = BotConstants::Callback::LIST_NEXT + ':' + std::to_string(titles[5].num);
layout.push_back({button_prev, button_next});
}
keyboard->inlineKeyboard = layout;
return keyboard;
}
TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createError(const std::string& errorCallback) {
auto keyboard = std::make_shared<TgBot::InlineKeyboardMarkup>();
TgBot::InlineKeyboardButton::Ptr button(new TgBot::InlineKeyboardButton);
button->text = BotConstants::Button::TO_MAIN_MENU;
button->callbackData = errorCallback;
keyboard->inlineKeyboard = {{button}};
return keyboard;
}

View file

@ -0,0 +1,35 @@
#include "front.hpp"
#include "KeyboardFactory.hpp"
#include "constants.hpp"
#include "handlers.hpp"
AnimeBot::AnimeBot(const std::string& token)
: bot(token)
, handler(bot.getApi()) {
setupHandlers();
}
void AnimeBot::setupHandlers() {
bot.getEvents().onCommand("start", [this](TgBot::Message::Ptr message) {
sendMainMenu(message->chat->id);
//TODO: производить инициализацию контекста только после авторизации
handler.initUser(message->from->id);
});
bot.getEvents().onCallbackQuery([this](TgBot::CallbackQuery::Ptr query) {
handler.handleCallback(query);
});
bot.getEvents().onAnyMessage([this](TgBot::Message::Ptr message) {
handler.handleMessage(message);
});
}
void AnimeBot::sendMainMenu(int64_t chatId) {
auto keyboard = KeyboardFactory::createMainMenu();
bot.getApi().sendMessage(chatId, BotConstants::Text::MAIN_MENU, nullptr, nullptr, keyboard);
}
TgBot::Bot& AnimeBot::getBot() {
return bot;
}

View file

@ -0,0 +1,219 @@
#include "handlers.hpp"
#include "KeyboardFactory.hpp"
#include "structs.hpp"
#include "constants.hpp"
void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) {
//const auto& current = ctx.history.back(); // текущий экран
const std::string& data = query->data;
int64_t userId = query->from->id;
int64_t chatId = query->message->chat->id;
int64_t messageId = query->message->messageId;
// Пагинация (в списках)
/* Временно отключаем, все равно не функционирует :)
if ((data == BotConstants::Callback::LIST_PREV || data == BotConstants::Callback::LIST_NEXT)
&& (current.state == UserState::VIEWING_MY_TITLES || current.state == UserState::VIEWING_REVIEW_LIST ||
current.state == UserState::VIEWING_FOUND_TITLES)) {
int64_t newPayload = current.payload;
if (data == BotConstants::Callback::LIST_PREV && newPayload > 0) {
reducePayload(newPayload, current.state);
} else if (data == BotConstants::Callback::LIST_NEXT) {
increasePayload(newPayload, current.state);
} else {
if (data == BotConstants::Callback::LIST_PREV) {
std::cout << "Error: navigation:prev callback for 1st page" << std::endl;
return;
}
// TODO: log
std::cout << "Error: navigation:prev unknown error" << std::endl;
}
ctx.history.back().payload = newPayload;
renderCurrent(query, ctx);
return;
}*/
// Обработка back по интерфейсу
if (data == BotConstants::Callback::NAV_BACK) {
if (!contextManager.popStep(userId)) {
sendError(chatId, messageId, BotConstants::Text::SAD_ERROR);
return;
}
renderCurrent(query);
return;
}
// Переходы вперёд (push)
std::optional<NavigationStep> currentStep = contextManager.getCurrentStep(userId);
if(!currentStep.has_value()) {
sendError(chatId, messageId, BotConstants::Text::SAD_ERROR);
return;
}
auto newStepOpt = computeNextStep(query, currentStep.value());
if (!newStepOpt.has_value()) {
sendError(chatId, messageId, BotConstants::Text::SAD_ERROR);
return;
}
contextManager.pushNavigationStep(userId, newStepOpt.value());
renderCurrent(query);
}
void BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query) {
int64_t userId = query->from->id;
int64_t chatId = query->message->chat->id;
int64_t messageId = query->message->messageId;
auto step = contextManager.getCurrentStep(userId);
if(!step.has_value()) {;
sendError(chatId, messageId, BotConstants::Text::SAD_ERROR);
return;
}
switch (step.value().state) {
case UserState::MAIN_MENU:
editMessage(chatId, messageId, showMainMenu());
return;
case UserState::VIEWING_MY_TITLES:
server_.fetchUserTitlesAsync(std::to_string(22)) // ALARM: тестовое значение вместо userId
.then([this, chatId, messageId](pplx::task<std::vector<BotStructs::Title>> t) {
try {
auto titles = t.get();
std::string message = formatTitlesList(titles);
auto keyboard = KeyboardFactory::createMyTitles(titles);
editMessage(chatId, messageId, {message, keyboard});
} catch (const std::exception& e) {
sendError(chatId, messageId, BotConstants::Text::SERVER_ERROR);
// Логирование ошибки (например, в cerr)
}
});
return;
case UserState::VIEWING_TITLE_PAGE:
server_.fetchTitleAsync(std::to_string(22), step.value().payload)
.then([this, chatId, messageId](pplx::task<BotStructs::Title> t) {
try {
auto title = t.get();
std::string message = formatTitle(title);
auto keyboard = KeyboardFactory::createTitleMenu(title.id);
std::string imageUrl = "https://i.pinimg.com/736x/30/2b/49/302b49176e5a74ef43871a462df47e1f.jpg";
editMessageWithPhoto(chatId, messageId, imageUrl, {message, keyboard});
} catch (const std::exception& e) {
sendError(chatId, messageId, BotConstants::Text::SERVER_ERROR);
// Логирование ошибки (например, в cerr)
}
});
return;
/*
case UserState::VIEWING_REVIEW:
return returnReview(step.payload); // payload = reviewId
case UserState::AWAITING_REVIEW:
return HandlerResult{"Пришлите текст отзыва:", nullptr};
// ...
*/
default:
return editMessage(chatId, messageId, HandlerResult{BotConstants::Text::SAD_ERROR, nullptr});
}
}
std::optional<NavigationStep> BotHandlers::computeNextStep(
const TgBot::CallbackQuery::Ptr& query,
const NavigationStep& current
) {
const std::string& data = query->data;
switch (current.state) {
case UserState::MAIN_MENU:
if (data == BotConstants::Callback::MY_TITLES) {
return NavigationStep{UserState::VIEWING_MY_TITLES, 0};
}
break;
case UserState::VIEWING_MY_TITLES:
if (data.starts_with(BotConstants::Callback::CHOICE)) {
int64_t titleId = parseId(data);
return NavigationStep{UserState::VIEWING_TITLE_PAGE, titleId};
}
break;
/*
case UserState::VIEWING_TITLE_PAGE:
if (data == BotConstants::Callback::ACTION_ADD_REVIEW) {
return NavigationStep{UserState::AWAITING_REVIEW, current.payload};
}
if (data.starts_with("review_")) {
int64_t reviewId = parseId(data);
return NavigationStep{UserState::VIEWING_REVIEW, reviewId};
}
break;
*/
default:
break;
}
return std::nullopt;
}
std::string BotHandlers::formatTitlesList(const std::vector<BotStructs::Title>& titles) {
if (titles.empty()) {
return "У вас пока нет тайтлов.";
}
std::string msg;
for (size_t i = 0; i < titles.size(); ++i) {
// num — 0-based, но в сообщении показываем 1-based
msg += std::to_string(i + 1) + ". " + titles[i].name + "\n";
}
return msg;
}
std::string BotHandlers::formatTitle(const BotStructs::Title& title) {
std::string msg;
msg += title.name + "\n";
return msg;
}
int64_t BotHandlers::parseId(const std::string& data) {
const std::string prefix = "choice:";
if (data.substr(0, prefix.size()) != prefix) {
throw std::invalid_argument("Data does not start with 'choice:'");
}
std::string idPart = data.substr(prefix.size());
// Проверяем, что остаток состоит только из цифр (и, возможно, знака '-')
if (idPart.empty()) {
throw std::invalid_argument("ID part is empty");
}
size_t startPos = 0;
if (idPart[0] == '-') {
if (idPart.size() == 1) {
throw std::invalid_argument("Invalid negative ID");
}
startPos = 1;
}
for (size_t i = startPos; i < idPart.size(); ++i) {
if (!std::isdigit(static_cast<unsigned char>(idPart[i]))) {
throw std::invalid_argument("ID contains non-digit characters");
}
}
try {
return std::stoll(idPart);
} catch (const std::out_of_range&) {
throw std::out_of_range("ID is out of range for int64_t");
}
}

View file

@ -0,0 +1,187 @@
#include "handlers.hpp"
#include "KeyboardFactory.hpp"
#include "structs.hpp"
#include "constants.hpp"
void BotHandlers::handleCallback(TgBot::CallbackQuery::Ptr query) {
if (!query) {
// TODO: log
return;
}
try {
// TODO: Тут mutex на многопоточке
botApi.answerCallbackQuery(query->id);
} catch (const std::exception& e) {
std::cerr << "answerCallbackQuery error";
//TODO: обработка ошибки
}
processCallbackImpl(query);
}
/* deprecated. will be deleted soon.
HandlerResult BotHandlers::returnMyTitles(int64_t userId, int64_t payload) {
// Здесь должен происходить запрос на сервер
std::vector<BotStructs::Title> titles = {{123, "Школа мертвяков", "", 1}, {321, "KissXsis", "", 2}};
struct HandlerResult result;
result.keyboard = KeyboardFactory::createMyTitles(titles);
result.message = "1. Школа мертвяков\n2. KissXsis\n";
return result;
}*/
void BotHandlers::handleMessage(TgBot::Message::Ptr message) {
//TODO: просмотр состояния пользователя
return;
}
void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) {
const std::string& data = query->data;
int64_t userId = query->from->id;
int64_t chatId = query->message->chat->id;
int64_t messageId = query->message->messageId;
std::optional<UserContext> ctx = contextManager.getContext(userId);
if (!ctx.has_value()) {
// TODO: log
sendError(chatId, messageId, BotConstants::Text::AUTH_ERROR);
std::cout << "Error: Не нашел пользователя " << userId;
return;
}
if (data.starts_with(BotConstants::Callback::NAVIGATION)) {
handleNavigation(query);
}
else if (data.starts_with(BotConstants::Callback::CHOICE)) {
handleNavigation(query);
}
else if (data.starts_with(BotConstants::Callback::ERROR)) {
handleError(query);
}
else {
botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR, nullptr, nullptr);
}
}
void BotHandlers::reducePayload(int64_t& payload, const UserState curState) {
if (curState == UserState::VIEWING_MY_TITLES ||
curState == UserState::VIEWING_FOUND_TITLES) {
payload -= BotConstants::DISP_TITLES_NUM;
} else if (curState == UserState::VIEWING_REVIEW_LIST) {
payload -= BotConstants::DISP_REVIEW_NUM;
} else {
// TODO: log
payload = BotConstants::NULL_PAYLOAD;
std::cerr << "Error: reducePayload" << std::endl;
}
}
void BotHandlers::increasePayload(int64_t& payload, const UserState curState) {
if (curState == UserState::VIEWING_MY_TITLES ||
curState == UserState::VIEWING_FOUND_TITLES) {
payload += BotConstants::DISP_TITLES_NUM;
} else if (curState == UserState::VIEWING_REVIEW_LIST) {
payload += BotConstants::DISP_REVIEW_NUM;
} else {
// TODO: log
payload = BotConstants::NULL_PAYLOAD;
std::cerr << "Error: increasePayload" << std::endl;
}
}
void BotHandlers::editMessage(int64_t chatId, int64_t messageId, HandlerResult response) {
// botApi.editMessageText быстрее. Реализовать его, где возможно
try {
botApi.deleteMessage(chatId, messageId);
} catch (const std::exception& e) {
std::cerr << "Warning: Failed to delete message " << messageId
<< " in chat " << chatId << ": " << e.what() << std::endl;
// Продолжаем отправку нового сообщения даже при ошибке удаления
}
// 2. Отправляем новое сообщение с правильным порядком параметров
botApi.sendMessage(
chatId, // chatId
response.message, // text
nullptr, // linkPreviewOptions (nullptr = default)
nullptr, // replyParameters (не отвечаем на сообщение)
response.keyboard, // replyMarkup — клавиатура
"HTML", // parseMode — поддержка <b>, <i> и т.д.
false // disableNotification
// остальные параметры по умолчанию
);
}
void BotHandlers::editMessageWithPhoto(
int64_t chatId,
int64_t messageId,
const std::string& imageUrl,
HandlerResult response)
{
// 1. Удаляем старое сообщение
try {
botApi.deleteMessage(chatId, messageId);
} catch (const std::exception& e) {
// Игнорируем ошибку, если сообщение уже удалено или недоступно
std::cerr << "Warning: Failed to delete message: " << e.what() << std::endl;
}
std::string safeCaption = response.message;
if (safeCaption.length() > 1024) {
safeCaption = safeCaption.substr(0, 1021) + "...";
}
// Отправка фото по URL
botApi.sendPhoto(
chatId,
imageUrl,
safeCaption,
nullptr,
response.keyboard,
"HTML"
);
}
HandlerResult BotHandlers::showMainMenu() {
auto keyboard = KeyboardFactory::createMainMenu();
return HandlerResult{BotConstants::Text::MAIN_MENU, keyboard};
}
void BotHandlers::sendError(int64_t chatId, int64_t messageId, const std::string& errText) {
//TODO: посылать сообщение с кнопкой возврата в главное меню
TgBot::InlineKeyboardMarkup::Ptr keyboard;
if (errText == BotConstants::Text::SAD_ERROR) {
keyboard = KeyboardFactory::createError(BotConstants::Callback::ERROR_NAVIGATION);
}
else if (errText == BotConstants::Text::AUTH_ERROR) {
keyboard = nullptr; //KeyboardFactory::createError(BotConstants::Callback::ERROR_AUTH);
}
editMessage(chatId, messageId, {errText, keyboard});
}
void BotHandlers::handleError(TgBot::CallbackQuery::Ptr query) {
const std::string& data = query->data;
int64_t userId = query->from->id;
int64_t chatId = query->message->chat->id;
int64_t messageId = query->message->messageId;
if(data == BotConstants::Callback::ERROR_NAVIGATION) {
contextManager.removeContext(userId);
contextManager.createInitContext(userId);
auto result = showMainMenu();
editMessage(chatId, messageId, result);
}
else if(data == BotConstants::Callback::ERROR_AUTH) {
// TODO: продумать логику
HandlerResult result = {BotConstants::Text::AUTH_ERROR, nullptr};
editMessage(chatId, messageId, result);
}
}
void BotHandlers::initUser(int64_t userId) {
contextManager.createInitContext(userId);
}

View file

@ -0,0 +1,15 @@
#include <front.hpp>
int main() {
AnimeBot bot(getenv("TOKEN"));
TgBot::TgLongPoll longPoll(bot.getBot());
while (true) {
try {
longPoll.start();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}
return 0;
}

View file

@ -28,16 +28,6 @@ server {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
} }
location /media/ {
rewrite ^/media/(.*)$ /$1 break;
proxy_pass http://nyanimedb-images:8000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
#error_page 404 /404.html; #error_page 404 /404.html;
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;

View file

@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import { useState, useEffect } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import UserPage from "./pages/UserPage/UserPage"; import UserPage from "./pages/UserPage/UserPage";
import TitlesPage from "./pages/TitlesPage/TitlesPage"; import TitlesPage from "./pages/TitlesPage/TitlesPage";
@ -12,26 +11,12 @@ import { Header } from "./components/Header/Header";
// OpenAPI.WITH_CREDENTIALS = true // OpenAPI.WITH_CREDENTIALS = true
const App: React.FC = () => { const App: React.FC = () => {
const [userId, setUserId] = useState<string | null>(localStorage.getItem("user_id")); const username = localStorage.getItem("username") || undefined;
const userId = localStorage.getItem("userId");
// 2. Listen for the same event the Header uses
useEffect(() => {
const handleAuthChange = () => {
setUserId(localStorage.getItem("user_id"));
};
window.addEventListener("storage", handleAuthChange);
window.addEventListener("local-storage-update", handleAuthChange);
return () => {
window.removeEventListener("storage", handleAuthChange);
window.removeEventListener("local-storage-update", handleAuthChange);
};
}, []);
return ( return (
<Router> <Router>
<Header /> <Header username={username} />
<Routes> <Routes>
{/* auth */} {/* auth */}
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />

View file

@ -2,7 +2,7 @@
import type { Client, Options as Options2, TDataShape } from './client'; import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen'; import { client } from './client.gen';
import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersData, GetUsersErrors, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUsersResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, PostMediaUploadData, PostMediaUploadErrors, PostMediaUploadResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen'; import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersData, GetUsersErrors, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUsersResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & { export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/** /**
@ -113,18 +113,3 @@ export const updateUserTitle = <ThrowOnError extends boolean = false>(options: O
...options.headers ...options.headers
} }
}); });
/**
* Upload an image (PNG, JPEG, or WebP)
*
* Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**.
*
*/
export const postMediaUpload = <ThrowOnError extends boolean = false>(options: Options<PostMediaUploadData, ThrowOnError>) => (options.client ?? client).post<PostMediaUploadResponses, PostMediaUploadErrors, ThrowOnError>({
url: '/media/upload',
...options,
headers: {
'Content-Type': 'encoding',
...options.headers
}
});

View file

@ -125,7 +125,7 @@ export type UserTitle = {
status: UserTitleStatus; status: UserTitleStatus;
rate?: number; rate?: number;
review_id?: number; review_id?: number;
ftime?: string; ctime?: string;
}; };
export type UserTitleMini = { export type UserTitleMini = {
@ -134,7 +134,7 @@ export type UserTitleMini = {
status: UserTitleStatus; status: UserTitleStatus;
rate?: number; rate?: number;
review_id?: number; review_id?: number;
ftime?: string; ctime?: string;
}; };
export type Review = { export type Review = {
@ -453,7 +453,6 @@ export type AddUserTitleData = {
title_id: number; title_id: number;
status: UserTitleStatus; status: UserTitleStatus;
rate?: number; rate?: number;
ftime?: string;
}; };
path: { path: {
/** /**
@ -579,7 +578,6 @@ export type UpdateUserTitleData = {
body: { body: {
status?: UserTitleStatus; status?: UserTitleStatus;
rate?: number; rate?: number;
ftime?: string;
}; };
path: { path: {
user_id: number; user_id: number;
@ -620,38 +618,3 @@ export type UpdateUserTitleResponses = {
}; };
export type UpdateUserTitleResponse = UpdateUserTitleResponses[keyof UpdateUserTitleResponses]; export type UpdateUserTitleResponse = UpdateUserTitleResponses[keyof UpdateUserTitleResponses];
export type PostMediaUploadData = {
body: unknown;
path?: never;
query?: never;
url: '/media/upload';
};
export type PostMediaUploadErrors = {
/**
* Bad request e.g., invalid/malformed image, empty file
*/
400: string;
/**
* Unsupported Media Type e.g., request `Content-Type` is not `multipart/form-data`,
* or the `image` part has an unsupported `Content-Type` (not image/png, image/jpeg, or image/webp)
*
*/
415: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type PostMediaUploadError = PostMediaUploadErrors[keyof PostMediaUploadErrors];
export type PostMediaUploadResponses = {
/**
* Image uploaded successfully
*/
200: Image;
};
export type PostMediaUploadResponse = PostMediaUploadResponses[keyof PostMediaUploadResponses];

View file

@ -1,16 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from './client';
import type { ClientOptions as ClientOptions2 } from './types.gen';
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: '/auth' }));

Some files were not shown because too many files have changed in this diff Show more