diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2b807ad..44a4d72 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,9 +20,8 @@ "charliermarsh.ruff", "d-biehl.robotcode", "tamasfe.even-better-toml", - "ms-azuretools.vscode-docker", - "Gruntfuggly.todo-tree", - "shardulm94.trailing-spaces" + "ms-azuretools.vscode-docker" + ] } } diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml index 30b2c26..ed5896f 100644 --- a/.github/workflows/on-push.yml +++ b/.github/workflows/on-push.yml @@ -53,8 +53,8 @@ jobs: strategy: matrix: os: [ 'ubuntu-latest', 'windows-latest'] - python-version: ['3.10', '3.11', '3.12', '3.13'] - robot-version: ['6.1.1', '7.3.2'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + robot-version: ['6.1.1', '7.4.1'] exclude: - os: 'windows-latest' python-version: '3.10' @@ -62,6 +62,8 @@ jobs: python-version: '3.11' - os: 'windows-latest' python-version: '3.12' + - os: 'windows-latest' + python-version: '3.13' - os: 'ubuntu-latest' python-version: '3.10' robot-version: '6.1.1' @@ -71,6 +73,9 @@ jobs: - os: 'ubuntu-latest' python-version: '3.12' robot-version: '6.1.1' + - os: 'ubuntu-latest' + python-version: '3.13' + robot-version: '6.1.1' fail-fast: false steps: - uses: actions/checkout@v6 @@ -96,7 +101,7 @@ jobs: tail: true wait-for: 1m - name: Run tests on latest RF 7 version - if: matrix.robot-version == '7.3.2' + if: matrix.robot-version == '7.4.1' run: | inv tests - name: Run tests on latest RF 6 version diff --git a/.gitignore b/.gitignore index 65fd3fe..26adcdd 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,10 @@ coverage.xml env/ venv/ -# IDE config +# IDE config and local tool settings .vscode/launch.json .vscode/settings.json +.robot.toml # default logs location for the repo tests/logs diff --git a/docs/coverage-badge.svg b/docs/coverage-badge.svg index 04c7fe6..7b1e43f 100644 --- a/docs/coverage-badge.svg +++ b/docs/coverage-badge.svg @@ -1 +1 @@ -coverage: 93.36%coverage93.36% \ No newline at end of file +coverage: 93.63%coverage93.63% \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index cfef690..f60aa21 100644 --- a/docs/index.html +++ b/docs/index.html @@ -644,10 +644,10 @@

The custom mappings file


 from OpenApiLibCore import (
     IGNORE,
-    Dto,
     IdDependency,
     IdReference,
     PathPropertiesConstraint,
+    RelationsMapping,
     PropertyValueConstraint,
     UniquePropertyValueConstraint,
 )
@@ -658,7 +658,7 @@ 

The custom mappings file

} -class MyDtoThatDoesNothing(Dto): +class MyMappingThatDoesNothing(RelationsMapping): @staticmethod def get_relations(): relations = [] @@ -675,13 +675,13 @@

The custom mappings file

return relations -DTO_MAPPING = { - ("/myspecialpath", "post"): MyDtoThatDoesNothing +RELATIONS_MAPPING = { + ("/myspecialpath", "post"): MyMappingThatDoesNothing } PATH_MAPPING = { - "/mypathwithexternalid/{external_id}": MyDtoThatDoesNothing + "/mypathwithexternalid/{external_id}": MyMappingThatDoesNothing }
@@ -693,13 +693,13 @@

The custom mappings file

Here the classes needed to implement custom mappings are imported. This section can just be copied without changes.
  • The ID_MAPPING "constant" definition / assignment.
  • -
  • The section defining the mapping Dtos. More on this later.
  • -
  • The DTO_MAPPING "constant" definition / assignment.
  • +
  • The section defining the RelationsMappings. More on this later.
  • +
  • The RELATIONS_MAPPING "constant" definition / assignment.
  • The PATH_MAPPING "constant" definition / assignment.
  • -

    The ID_MAPPING, DTO_MAPPING and PATH_MAPPING

    -When a custom mappings file is used, the OpenApiLibCore will attempt to import it and then import DTO_MAPPING, PATH_MAPPING and ID_MAPPING from it. +

    The ID_MAPPING, RELATIONS_MAPPING and PATH_MAPPING

    +When a custom mappings file is used, the OpenApiLibCore will attempt to import it and then import RELATIONS_MAPPING, PATH_MAPPING and ID_MAPPING from it. For this reason, the exact same name must be used in a custom mappings file (capitilization matters).

    The ID_MAPPING

    @@ -721,18 +721,18 @@

    The ID_MAPPING

    -

    The DTO_MAPPING

    -The DTO_MAPPING is a dictionary with a tuple as its key and a mappings Dto as its value. +

    The RELATIONS_MAPPING

    +The RELATIONS_MAPPING is a dictionary with a tuple as its key and a RelationsMapping as its value. The tuple must be in the form ("path_from_the_paths_section", "method_supported_by_the_path"). The path_from_the_paths_section must be exactly as found in the openapi document. The method_supported_by_the_path must be one of the methods supported by the path and must be in lowercase.

    The PATH_MAPPING

    -The PATH_MAPPING is a dictionary with a "path_from_the_paths_section" as its key and a mappings Dto as its value. +The PATH_MAPPING is a dictionary with a "path_from_the_paths_section" as its key and a RelationsMapping as its value. The path_from_the_paths_section must be exactly as found in the openapi document. -

    Dto mapping classes

    +

    RelationsMapping classes

    As can be seen from the import section above, a number of classes are available to deal with relations between resources and / or constraints on properties. Each of these classes is designed to handle a relation or constraint commonly seen in REST APIs. @@ -757,7 +757,7 @@

    IdReference

    This relation can be implemented as follows:
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -769,8 +769,8 @@ 

    IdReference

    ] return relations -DTO_MAPPING = { - ("/employees", "post"): EmployeeDto +RELATIONS_MAPPING = { + ("/employees", "post"): EmployeeMapping }
    @@ -801,7 +801,7 @@

    IdDependency

    To verify that the specified error_code indeed occurs when attempting to delete the Wagegroup, we can implement the following dependency:
    
    -class WagegroupDto(Dto):
    +class WagegroupMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -813,8 +813,8 @@ 

    IdDependency

    ] return relations -DTO_MAPPING = { - ("/wagegroups/{wagegroup_id}", "delete"): WagegroupDto +RELATIONS_MAPPING = { + ("/wagegroups/{wagegroup_id}", "delete"): WagegroupMapping }
    @@ -833,7 +833,7 @@

    UniquePropertyValueConstraint

    To verify that the specified error_code occurs when attempting to post an Employee with an employee_number that is already in use, we can implement the following dependency:
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -845,15 +845,15 @@ 

    UniquePropertyValueConstraint

    ] return relations -DTO_MAPPING = { - ("/employees", "post"): EmployeeDto, - ("/employees/${employee_id}", "put"): EmployeeDto, - ("/employees/${employee_id}", "patch"): EmployeeDto, +RELATIONS_MAPPING = { + ("/employees", "post"): EmployeeMapping, + ("/employees/${employee_id}", "put"): EmployeeMapping, + ("/employees/${employee_id}", "patch"): EmployeeMapping, }
    -Note how this example reuses the EmployeeDto to model the uniqueness constraint for all the operations (post, put and patch) that all relate to the same employee_number. +Note how this example reuses the EmployeeMapping to model the uniqueness constraint for all the operations (post, put and patch) that all relate to the same employee_number.
    @@ -867,7 +867,7 @@

    PropertyValueConstraint

    This type of constraint can be modeled as follows:
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -879,10 +879,10 @@ 

    PropertyValueConstraint

    ] return relations -DTO_MAPPING = { - ("/employees", "post"): EmployeeDto, - ("/employees/${employee_id}", "put"): EmployeeDto, - ("/employees/${employee_id}", "patch"): EmployeeDto, +RELATIONS_MAPPING = { + ("/employees", "post"): EmployeeMapping, + ("/employees/${employee_id}", "put"): EmployeeMapping, + ("/employees/${employee_id}", "patch"): EmployeeMapping, }
    @@ -891,7 +891,7 @@

    PropertyValueConstraint

    To support additional restrictions like these, the PropertyValueConstraint supports two additional properties: error_value and invalid_value_error_code:
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -905,10 +905,10 @@ 

    PropertyValueConstraint

    ] return relations -DTO_MAPPING = { - ("/employees", "post"): EmployeeDto, - ("/employees/${employee_id}", "put"): EmployeeDto, - ("/employees/${employee_id}", "patch"): EmployeeDto, +RELATIONS_MAPPING = { + ("/employees", "post"): EmployeeMapping, + ("/employees/${employee_id}", "put"): EmployeeMapping, + ("/employees/${employee_id}", "patch"): EmployeeMapping, }
    @@ -920,7 +920,7 @@

    PropertyValueConstraint

    This situation can be handled by use of the special IGNORE value (see below for other uses):
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -934,10 +934,10 @@ 

    PropertyValueConstraint

    ] return relations -DTO_MAPPING = { - ("/employees", "post"): EmployeeDto, - ("/employees/${employee_id}", "put"): EmployeeDto, - ("/employees/${employee_id}", "patch"): EmployeeDto, +RELATIONS_MAPPING = { + ("/employees", "post"): EmployeeMapping, + ("/employees/${employee_id}", "put"): EmployeeMapping, + ("/employees/${employee_id}", "patch"): EmployeeMapping, }
    @@ -950,7 +950,7 @@

    PathPropertiesConstraint

    Just use this for the path
    -Note: The PathPropertiesConstraint is only applicable to the get_path_relations in a Dto and only the PATH_MAPPING uses the get_path_relations. +Note: The PathPropertiesConstraint is only applicable to the get_path_relations in a RelationsMapping and only the PATH_MAPPING uses the get_path_relations.
    To be able to automatically perform endpoint validations, the OpenApiLibCore has to construct the url for the resource from the path as found in the openapi document. @@ -970,7 +970,7 @@

    PathPropertiesConstraint

    It should be clear that the OpenApiLibCore won't be able to acquire a valid month and date. The PathPropertiesConstraint can be used in this case:
    
    -class BirthdaysDto(Dto):
    +class BirthdaysMapping(RelationsMapping):
         @staticmethod
         def get_path_relations():
             relations = [
    @@ -979,7 +979,7 @@ 

    PathPropertiesConstraint

    return relations PATH_MAPPING = { - "/birthdays/{month}/{date}": BirthdaysDto + "/birthdays/{month}/{date}": BirthdaysMapping }
    @@ -999,7 +999,7 @@

    IGNORE

    To prevent OpenApiLibCore from generating invalid combinations of path and query parameters in this type of endpoint, the IGNORE special value can be used to ensure the related query parameter is never send in a request.
    
    -class EnergyLabelDto(Dto):
    +class EnergyLabelMapping(RelationsMapping):
         @staticmethod
         def get_parameter_relations():
             relations = [
    @@ -1018,8 +1018,8 @@ 

    IGNORE

    ] return relations -DTO_MAPPING = { - ("/energy_label/{zipcode}/{home_number}", "get"): EnergyLabelDto, +RELATIONS_MAPPING = { + ("/energy_label/{zipcode}/{home_number}", "get"): EnergyLabelMapping, }
    @@ -1032,7 +1032,7 @@

    IGNORE

    Such situations can be handled by a mapping as shown below:

    
    -class PatchEmployeeDto(Dto):
    +class PatchEmployeeMapping(RelationsMapping):
         @staticmethod
         def get_parameter_relations() -> list[ResourceRelation]:
             relations: list[ResourceRelation] = [
    @@ -1051,8 +1051,8 @@ 

    IGNORE

    ] return relations -DTO_MAPPING = { - ("/employees/{employee_id}", "patch"): PatchEmployeeDto, +RELATIONS_MAPPING = { + ("/employees/{employee_id}", "patch"): PatchEmployeeMapping, }
    diff --git a/docs/openapi_libcore.html b/docs/openapi_libcore.html index bc4c6cf..16e2c07 100644 --- a/docs/openapi_libcore.html +++ b/docs/openapi_libcore.html @@ -1,410 +1,387 @@ - - - - - - - - - + + + + + + + - - - - + + + + -
    -

    Opening library documentation failed

    - -
    +
    +

    Opening library documentation failed

    + +
    - + - - - - + + + + -
    - - - - - - - - + + + + + + + - - - - - - + + + + + - - + data-v-2754030d="" fill="var(--text-color)">`,t.classList.add("modal-close-button");let r=document.createElement("div");r.classList.add("modal-close-button-container"),r.appendChild(t),t.addEventListener("click",()=>{rl()}),e.appendChild(r),r.addEventListener("click",()=>{rl()});let n=document.createElement("div");n.id="modal",n.classList.add("modal"),n.addEventListener("click",({target:e})=>{"A"===e.tagName.toUpperCase()&&rl()});let o=document.createElement("div");o.id="modal-content",o.classList.add("modal-content"),n.appendChild(o),e.appendChild(n),document.body.appendChild(e),document.addEventListener("keydown",({key:e})=>{"Escape"===e&&rl()})}()}renderTemplates(){this.renderLibdocTemplate("base",this.libdoc,"#root"),this.libdoc.inits.length>0&&this.renderImporting(),this.renderShortcuts(),this.renderKeywords(),this.renderLibdocTemplate("data-types"),this.renderLibdocTemplate("footer")}initHashEvents(){window.addEventListener("hashchange",function(){document.getElementsByClassName("hamburger-menu")[0].checked=!1},!1),window.addEventListener("hashchange",function(){if(0==window.location.hash.indexOf("#type-")){let e="#type-modal-"+decodeURI(window.location.hash.slice(6)),t=document.querySelector(".data-types").querySelector(e);t&&rs(t)}},!1),this.scrollToHash()}initTagSearch(){let e=new URLSearchParams(window.location.search),t="";e.has("tag")&&(t=e.get("tag"),this.tagSearch(t,window.location.hash)),this.libdoc.tags.length&&(this.libdoc.selectedTag=t,this.renderLibdocTemplate("tags-shortcuts"),document.getElementById("tags-shortcuts-container").onchange=e=>{let t=e.target.selectedOptions[0].value;""!=t?this.tagSearch(t):this.clearTagSearch()})}initLanguageMenu(){this.renderTemplate("language",{languages:this.translations.getLanguageCodes()}),document.querySelectorAll("#language-container ul a").forEach(e=>{e.innerHTML===this.translations.currentLanguage()&&e.classList.toggle("selected"),e.addEventListener("click",()=>{this.translations.setLanguage(e.innerHTML)&&this.render()})}),document.querySelector("#language-container button").addEventListener("click",()=>{document.querySelector("#language-container ul").classList.toggle("hidden")})}renderImporting(){this.renderLibdocTemplate("importing"),this.registerTypeDocHandlers("#importing-container")}renderShortcuts(){this.renderLibdocTemplate("shortcuts"),document.getElementById("toggle-keyword-shortcuts").addEventListener("click",()=>this.toggleShortcuts()),document.querySelector(".clear-search").addEventListener("click",()=>this.clearSearch()),document.querySelector(".search-input").addEventListener("keydown",()=>rc(()=>this.searching(),150)),this.renderLibdocTemplate("keyword-shortcuts"),document.querySelectorAll("a.match").forEach(e=>e.addEventListener("click",this.closeMenu))}registerTypeDocHandlers(e){document.querySelectorAll(`${e} a.type`).forEach(e=>e.addEventListener("click",e=>{let t=e.target.dataset.typedoc;rs(document.querySelector(`#type-modal-${t}`))}))}renderKeywords(e=null){null==e&&(e=this.libdoc),this.renderLibdocTemplate("keywords",e),document.querySelectorAll(".kw-tags span").forEach(e=>{e.addEventListener("click",e=>{this.tagSearch(e.target.innerText)})}),this.registerTypeDocHandlers("#keywords-container"),document.getElementById("keyword-statistics-header").innerText=""+this.libdoc.keywords.length}setTheme(){document.documentElement.setAttribute("data-theme",this.getTheme())}getTheme(){return null!=this.libdoc.theme?this.libdoc.theme:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}scrollToHash(){if(window.location.hash){let e=window.location.hash.substring(1),t=document.getElementById(decodeURIComponent(e));null!=t&&t.scrollIntoView()}}tagSearch(e,t){document.getElementsByClassName("search-input")[0].value="";let r={tags:!0,tagsExact:!0},n=window.location.pathname+"?tag="+e+(t||"");this.markMatches(e,r),this.highlightMatches(e,r),history.replaceState&&history.replaceState(null,"",n),document.getElementById("keyword-shortcuts-container").scrollTop=0}clearTagSearch(){document.getElementsByClassName("search-input")[0].value="",history.replaceState&&history.replaceState(null,"",window.location.pathname),this.resetKeywords()}searching(){this.searchTime=Date.now();let e=document.getElementsByClassName("search-input")[0].value,t={name:!0,args:!0,doc:!0,tags:!0};e?requestAnimationFrame(()=>{this.markMatches(e,t,this.searchTime,()=>{this.highlightMatches(e,t,this.searchTime),document.getElementById("keyword-shortcuts-container").scrollTop=0})}):this.resetKeywords()}highlightMatches(e,t,n){if(n&&n!==this.searchTime)return;let o=document.querySelectorAll("#shortcuts-container .match"),i=document.querySelectorAll("#keywords-container .match");if(t.name&&(new(r(ef))(o).mark(e),new(r(ef))(i).mark(e)),t.args&&new(r(ef))(document.querySelectorAll("#keywords-container .match .args")).mark(e),t.doc&&new(r(ef))(document.querySelectorAll("#keywords-container .match .doc")).mark(e),t.tags){let n=document.querySelectorAll("#keywords-container .match .tags a, #tags-shortcuts-container .match .tags a");if(t.tagsExact){let t=[];n.forEach(r=>{r.textContent?.toUpperCase()==e.toUpperCase()&&t.push(r)}),new(r(ef))(t).mark(e)}else new(r(ef))(n).mark(e)}}markMatches(e,t,r,n){if(r&&r!==this.searchTime)return;let o=e.replace(/[-[\]{}()+?*.,\\^$|#]/g,"\\$&");t.tagsExact&&(o="^"+o+"$");let i=RegExp(o,"i"),a=i.test.bind(i),s={},l=0;s.keywords=this.libdoc.keywords.map(e=>{let r={...e};return r.hidden=!(t.name&&a(r.name))&&!(t.args&&a(r.args))&&!(t.doc&&a(r.doc))&&!(t.tags&&r.tags.some(a)),!r.hidden&&l++,r}),this.renderLibdocTemplate("keyword-shortcuts",s),this.renderKeywords(s),this.libdoc.tags.length&&(this.libdoc.selectedTag=t.tagsExact?e:"",this.renderLibdocTemplate("tags-shortcuts")),document.getElementById("keyword-statistics-header").innerText=l+" / "+s.keywords.length,0===l&&(document.querySelector("#keywords-container table").innerHTML=""),n&&requestAnimationFrame(n)}closeMenu(){document.getElementById("hamburger-menu-input").checked=!1}openKeywordWall(){document.getElementsByClassName("shortcuts")[0].classList.add("keyword-wall"),this.storage.set("keyword-wall","open"),document.getElementById("toggle-keyword-shortcuts").innerText="-"}closeKeywordWall(){document.getElementsByClassName("shortcuts")[0].classList.remove("keyword-wall"),this.storage.set("keyword-wall","close"),document.getElementById("toggle-keyword-shortcuts").innerText="+"}toggleShortcuts(){document.getElementsByClassName("shortcuts")[0].classList.contains("keyword-wall")?this.closeKeywordWall():this.openKeywordWall()}resetKeywords(){this.renderLibdocTemplate("keyword-shortcuts"),this.renderKeywords(),this.libdoc.tags.length&&(this.libdoc.selectedTag="",this.renderLibdocTemplate("tags-shortcuts")),history.replaceState&&history.replaceState(null,"",location.pathname)}clearSearch(){document.getElementsByClassName("search-input")[0].value="";let e=document.getElementById("tags-shortcuts-container");e&&(e.selectedIndex=0),this.resetKeywords()}renderLibdocTemplate(e,t=null,r=""){null==t&&(t=this.libdoc),this.renderTemplate(e,t,r)}renderTemplate(e,t,n=""){let o=document.getElementById(`${e}-template`)?.innerHTML,i=r(eg).compile(o);""===n&&(n=`#${e}-container`),document.body.querySelector(n).innerHTML=i(t)}},rh=libdoc;const rp=new eh("libdoc"),rd=ed.getInstance(rh.lang);new ru(rh,rp,rd).render(); + + + diff --git a/docs/openapidriver.html b/docs/openapidriver.html index 9bd63ad..2b53020 100644 --- a/docs/openapidriver.html +++ b/docs/openapidriver.html @@ -1,410 +1,387 @@ - - - - - - - - - + + + + + + + - - - - + + + + -
    -

    Opening library documentation failed

    - -
    +
    +

    Opening library documentation failed

    + +
    - + - - - - + + + + -
    - - - - - - - - + + + + + + + - - - - - - + + + + + - - + data-v-2754030d="" fill="var(--text-color)">`,t.classList.add("modal-close-button");let r=document.createElement("div");r.classList.add("modal-close-button-container"),r.appendChild(t),t.addEventListener("click",()=>{rl()}),e.appendChild(r),r.addEventListener("click",()=>{rl()});let n=document.createElement("div");n.id="modal",n.classList.add("modal"),n.addEventListener("click",({target:e})=>{"A"===e.tagName.toUpperCase()&&rl()});let o=document.createElement("div");o.id="modal-content",o.classList.add("modal-content"),n.appendChild(o),e.appendChild(n),document.body.appendChild(e),document.addEventListener("keydown",({key:e})=>{"Escape"===e&&rl()})}()}renderTemplates(){this.renderLibdocTemplate("base",this.libdoc,"#root"),this.libdoc.inits.length>0&&this.renderImporting(),this.renderShortcuts(),this.renderKeywords(),this.renderLibdocTemplate("data-types"),this.renderLibdocTemplate("footer")}initHashEvents(){window.addEventListener("hashchange",function(){document.getElementsByClassName("hamburger-menu")[0].checked=!1},!1),window.addEventListener("hashchange",function(){if(0==window.location.hash.indexOf("#type-")){let e="#type-modal-"+decodeURI(window.location.hash.slice(6)),t=document.querySelector(".data-types").querySelector(e);t&&rs(t)}},!1),this.scrollToHash()}initTagSearch(){let e=new URLSearchParams(window.location.search),t="";e.has("tag")&&(t=e.get("tag"),this.tagSearch(t,window.location.hash)),this.libdoc.tags.length&&(this.libdoc.selectedTag=t,this.renderLibdocTemplate("tags-shortcuts"),document.getElementById("tags-shortcuts-container").onchange=e=>{let t=e.target.selectedOptions[0].value;""!=t?this.tagSearch(t):this.clearTagSearch()})}initLanguageMenu(){this.renderTemplate("language",{languages:this.translations.getLanguageCodes()}),document.querySelectorAll("#language-container ul a").forEach(e=>{e.innerHTML===this.translations.currentLanguage()&&e.classList.toggle("selected"),e.addEventListener("click",()=>{this.translations.setLanguage(e.innerHTML)&&this.render()})}),document.querySelector("#language-container button").addEventListener("click",()=>{document.querySelector("#language-container ul").classList.toggle("hidden")})}renderImporting(){this.renderLibdocTemplate("importing"),this.registerTypeDocHandlers("#importing-container")}renderShortcuts(){this.renderLibdocTemplate("shortcuts"),document.getElementById("toggle-keyword-shortcuts").addEventListener("click",()=>this.toggleShortcuts()),document.querySelector(".clear-search").addEventListener("click",()=>this.clearSearch()),document.querySelector(".search-input").addEventListener("keydown",()=>rc(()=>this.searching(),150)),this.renderLibdocTemplate("keyword-shortcuts"),document.querySelectorAll("a.match").forEach(e=>e.addEventListener("click",this.closeMenu))}registerTypeDocHandlers(e){document.querySelectorAll(`${e} a.type`).forEach(e=>e.addEventListener("click",e=>{let t=e.target.dataset.typedoc;rs(document.querySelector(`#type-modal-${t}`))}))}renderKeywords(e=null){null==e&&(e=this.libdoc),this.renderLibdocTemplate("keywords",e),document.querySelectorAll(".kw-tags span").forEach(e=>{e.addEventListener("click",e=>{this.tagSearch(e.target.innerText)})}),this.registerTypeDocHandlers("#keywords-container"),document.getElementById("keyword-statistics-header").innerText=""+this.libdoc.keywords.length}setTheme(){document.documentElement.setAttribute("data-theme",this.getTheme())}getTheme(){return null!=this.libdoc.theme?this.libdoc.theme:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}scrollToHash(){if(window.location.hash){let e=window.location.hash.substring(1),t=document.getElementById(decodeURIComponent(e));null!=t&&t.scrollIntoView()}}tagSearch(e,t){document.getElementsByClassName("search-input")[0].value="";let r={tags:!0,tagsExact:!0},n=window.location.pathname+"?tag="+e+(t||"");this.markMatches(e,r),this.highlightMatches(e,r),history.replaceState&&history.replaceState(null,"",n),document.getElementById("keyword-shortcuts-container").scrollTop=0}clearTagSearch(){document.getElementsByClassName("search-input")[0].value="",history.replaceState&&history.replaceState(null,"",window.location.pathname),this.resetKeywords()}searching(){this.searchTime=Date.now();let e=document.getElementsByClassName("search-input")[0].value,t={name:!0,args:!0,doc:!0,tags:!0};e?requestAnimationFrame(()=>{this.markMatches(e,t,this.searchTime,()=>{this.highlightMatches(e,t,this.searchTime),document.getElementById("keyword-shortcuts-container").scrollTop=0})}):this.resetKeywords()}highlightMatches(e,t,n){if(n&&n!==this.searchTime)return;let o=document.querySelectorAll("#shortcuts-container .match"),i=document.querySelectorAll("#keywords-container .match");if(t.name&&(new(r(ef))(o).mark(e),new(r(ef))(i).mark(e)),t.args&&new(r(ef))(document.querySelectorAll("#keywords-container .match .args")).mark(e),t.doc&&new(r(ef))(document.querySelectorAll("#keywords-container .match .doc")).mark(e),t.tags){let n=document.querySelectorAll("#keywords-container .match .tags a, #tags-shortcuts-container .match .tags a");if(t.tagsExact){let t=[];n.forEach(r=>{r.textContent?.toUpperCase()==e.toUpperCase()&&t.push(r)}),new(r(ef))(t).mark(e)}else new(r(ef))(n).mark(e)}}markMatches(e,t,r,n){if(r&&r!==this.searchTime)return;let o=e.replace(/[-[\]{}()+?*.,\\^$|#]/g,"\\$&");t.tagsExact&&(o="^"+o+"$");let i=RegExp(o,"i"),a=i.test.bind(i),s={},l=0;s.keywords=this.libdoc.keywords.map(e=>{let r={...e};return r.hidden=!(t.name&&a(r.name))&&!(t.args&&a(r.args))&&!(t.doc&&a(r.doc))&&!(t.tags&&r.tags.some(a)),!r.hidden&&l++,r}),this.renderLibdocTemplate("keyword-shortcuts",s),this.renderKeywords(s),this.libdoc.tags.length&&(this.libdoc.selectedTag=t.tagsExact?e:"",this.renderLibdocTemplate("tags-shortcuts")),document.getElementById("keyword-statistics-header").innerText=l+" / "+s.keywords.length,0===l&&(document.querySelector("#keywords-container table").innerHTML=""),n&&requestAnimationFrame(n)}closeMenu(){document.getElementById("hamburger-menu-input").checked=!1}openKeywordWall(){document.getElementsByClassName("shortcuts")[0].classList.add("keyword-wall"),this.storage.set("keyword-wall","open"),document.getElementById("toggle-keyword-shortcuts").innerText="-"}closeKeywordWall(){document.getElementsByClassName("shortcuts")[0].classList.remove("keyword-wall"),this.storage.set("keyword-wall","close"),document.getElementById("toggle-keyword-shortcuts").innerText="+"}toggleShortcuts(){document.getElementsByClassName("shortcuts")[0].classList.contains("keyword-wall")?this.closeKeywordWall():this.openKeywordWall()}resetKeywords(){this.renderLibdocTemplate("keyword-shortcuts"),this.renderKeywords(),this.libdoc.tags.length&&(this.libdoc.selectedTag="",this.renderLibdocTemplate("tags-shortcuts")),history.replaceState&&history.replaceState(null,"",location.pathname)}clearSearch(){document.getElementsByClassName("search-input")[0].value="";let e=document.getElementById("tags-shortcuts-container");e&&(e.selectedIndex=0),this.resetKeywords()}renderLibdocTemplate(e,t=null,r=""){null==t&&(t=this.libdoc),this.renderTemplate(e,t,r)}renderTemplate(e,t,n=""){let o=document.getElementById(`${e}-template`)?.innerHTML,i=r(eg).compile(o);""===n&&(n=`#${e}-container`),document.body.querySelector(n).innerHTML=i(t)}},rh=libdoc;const rp=new eh("libdoc"),rd=ed.getInstance(rh.lang);new ru(rh,rp,rd).render(); + + + diff --git a/docs/releases.md b/docs/releases.md index fc45a8a..77136e4 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,14 +1,60 @@ # Release notes -## OpenApiTools v1.0.5 +## OpenApiTools v2.0.0 + +### Major changes and new features +- Request bodies now support all JSON types, not just `objects` (`dicts`). + - This closes [issue #9: No body generated when root is a list](https://github.com/MarketSquare/robotframework-openapitools/issues/9). + - The `Relations` still need to be reworked to align with this change. +- Refactored retrieving / loading of the OpenAPI spec. + - This closes [issue #93: SSL error even if cert / verify is set](https://github.com/MarketSquare/robotframework-openapitools/issues/93). +- Added keywords to make it easier to work with the `RequestValues` object: + - `Get Request Values Object` can be used to create a `RequestValues` instance from pre-defined values (where `Get Request Values` generates all values automatically). + - `Perform Authorized Request` is functionally the same as exisiting `Authorized Request` keyword, but it accepts a `RequestValues` instance as argument. + - `Validated Request` is functionally the same as the existing `Perform Validated Request` keyword, but it accepts the data as separate arguments instead of the `RequestValues`. + - `Convert Request Values To Dict` can be used to get a (Python) dict represenation of a `RequestValues` object that can be used with e.g. the Collections keywords for working with dictionaries. + - Thise closes [issue #98: Add keywords to simplify using Authorized Request and Perform Validated Request](https://github.com/MarketSquare/robotframework-openapitools/issues/98). +- Improved handling of `treat_as_mandatory` on a `PropertyValueConstraint`. +- Added support for using `IGNORE` as `invalid_value` on a `PropertyValueConstraint`. ### Bugfixes -- `parameters` at path level are not taken into account at operation level +- Added support for the `nullable` property in OAS 3.0 schemas when generating data. + - This closes [issue #81: nullable not taken into account in get_valid_value](https://github.com/MarketSquare/robotframework-openapitools/issues/81). +- Support added for multiple instances of OpenApiLibCore within the same suite. + - This closes [issue #96: Multiple keywords with same name error when using multiple generated libraries](https://github.com/MarketSquare/robotframework-openapitools/issues/96). +- Fixed validation errors caused by `Content-Type` not being handled case-insensitive. +- Fixed an exception during validation caused by `charset` being included in the `Content-Type` header for `application/json`. + +### Breaking changes +- Addressing [issue #95: Refactor: better name for Dto](https://github.com/MarketSquare/robotframework-openapitools/issues/95) introduces a number breaking renames: + - `Dto` has been renamed to `RelationsMapping`. + - `constraint_mapping` has been renamed to `relations_mapping` in a number of places. + - `DTO_MAPPING` has been renamed to `RELATIONS_MAPPING`. +- The `RequestData` class that is returned by a number of keywords has been changed: + - The `dto` property was removed. + - The `valid_data` property was added. + - The `relations_mapping` property was added. +- `invalid_property_default_response` library parameter renamed to `invalid_data_default_response`. + +### Additional changes +- Special handling of `"format": "byte"` for `"type": "string"` (OAS 3.0) was removed. + - While some logic related to this worked, the result was never JSON-serializable. +- The devcontainer setup was updated. +- The GitHub pipeline was updated to include Python 3.14. +- Updated minimum version markers for many dependencies. +- Annotations are now complete (as far as possible under Python 3.10).


    ## Previous versions +### OpenApiTools v1.0.5 + +#### Bugfixes +- `parameters` at path level are not taken into account at operation level. + +--- + ### OpenApiTools v1.0.4 #### Bugfixes @@ -41,7 +87,7 @@ ### OpenApiTools v1.0.1 #### Bugfixes -- `openapitools_docs` was missing from package distribution +- `openapitools_docs` was missing from package distribution. --- diff --git a/poetry.lock b/poetry.lock index 7c67acb..af9639b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,15 +1,15 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "annotated-doc" -version = "0.0.3" +version = "0.0.4" description = "Document parameters, class attributes, return types, and variables inline, with Annotated." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580"}, - {file = "annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda"}, + {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, + {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, ] [[package]] @@ -26,35 +26,34 @@ files = [ [[package]] name = "anyio" -version = "4.11.0" +version = "4.12.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, - {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, + {file = "anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb"}, + {file = "anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" -sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -trio = ["trio (>=0.31.0)"] +trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] [[package]] name = "astroid" -version = "4.0.1" +version = "4.0.2" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.10.0" groups = ["lint-and-format"] files = [ - {file = "astroid-4.0.1-py3-none-any.whl", hash = "sha256:37ab2f107d14dc173412327febf6c78d39590fdafcb44868f03b6c03452e3db0"}, - {file = "astroid-4.0.1.tar.gz", hash = "sha256:0d778ec0def05b935e198412e62f9bcca8b3b5c39fdbe50b0ba074005e477aab"}, + {file = "astroid-4.0.2-py3-none-any.whl", hash = "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b"}, + {file = "astroid-4.0.2.tar.gz", hash = "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070"}, ] [package.dependencies] @@ -72,64 +71,16 @@ files = [ {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, ] -[[package]] -name = "black" -version = "25.9.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7"}, - {file = "black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92"}, - {file = "black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713"}, - {file = "black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1"}, - {file = "black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa"}, - {file = "black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d"}, - {file = "black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608"}, - {file = "black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f"}, - {file = "black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0"}, - {file = "black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4"}, - {file = "black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e"}, - {file = "black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a"}, - {file = "black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175"}, - {file = "black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f"}, - {file = "black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831"}, - {file = "black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357"}, - {file = "black-25.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47"}, - {file = "black-25.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823"}, - {file = "black-25.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140"}, - {file = "black-25.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933"}, - {file = "black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae"}, - {file = "black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -pytokens = ">=0.1.10" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" -version = "2025.10.5" +version = "2025.11.12" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, ] [[package]] @@ -269,14 +220,14 @@ files = [ [[package]] name = "click" -version = "8.3.0" +version = "8.3.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev", "lint-and-format", "type-checking"] files = [ - {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, - {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, ] [package.dependencies] @@ -297,104 +248,104 @@ markers = {main = "platform_system == \"Windows\"", lint-and-format = "platform_ [[package]] name = "coverage" -version = "7.11.0" +version = "7.13.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31"}, - {file = "coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841"}, - {file = "coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf"}, - {file = "coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969"}, - {file = "coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847"}, - {file = "coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e"}, - {file = "coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c"}, - {file = "coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9"}, - {file = "coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745"}, - {file = "coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1"}, - {file = "coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2"}, - {file = "coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5"}, - {file = "coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0"}, - {file = "coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad"}, - {file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}, - {file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}, - {file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}, - {file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}, - {file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}, - {file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}, - {file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}, - {file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}, - {file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}, - {file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}, - {file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}, - {file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}, - {file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}, - {file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}, - {file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}, - {file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}, - {file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}, - {file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}, - {file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}, - {file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}, - {file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}, - {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, + {file = "coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070"}, + {file = "coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8"}, + {file = "coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f"}, + {file = "coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303"}, + {file = "coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820"}, + {file = "coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753"}, + {file = "coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b"}, + {file = "coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe"}, + {file = "coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7"}, + {file = "coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf"}, + {file = "coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd"}, + {file = "coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef"}, + {file = "coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae"}, + {file = "coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080"}, + {file = "coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf"}, + {file = "coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511"}, + {file = "coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1"}, + {file = "coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a"}, + {file = "coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6"}, + {file = "coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a"}, + {file = "coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e"}, + {file = "coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46"}, + {file = "coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39"}, + {file = "coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e"}, + {file = "coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256"}, + {file = "coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927"}, + {file = "coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f"}, + {file = "coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc"}, + {file = "coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b"}, + {file = "coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28"}, + {file = "coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2"}, + {file = "coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7"}, + {file = "coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc"}, + {file = "coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a"}, + {file = "coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904"}, + {file = "coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936"}, ] [package.dependencies] @@ -433,27 +384,27 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "docutils" -version = "0.22.2" +version = "0.22.4" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8"}, - {file = "docutils-0.22.2.tar.gz", hash = "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d"}, + {file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"}, + {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}, ] [[package]] name = "exceptiongroup" -version = "1.3.0" +version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version == \"3.10\"" +markers = "python_version < \"3.11\"" files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, ] [package.dependencies] @@ -464,14 +415,14 @@ test = ["pytest (>=6)"] [[package]] name = "faker" -version = "37.12.0" +version = "39.0.0" description = "Faker is a Python package that generates fake data for you." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "faker-37.12.0-py3-none-any.whl", hash = "sha256:afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4"}, - {file = "faker-37.12.0.tar.gz", hash = "sha256:7505e59a7e02fa9010f06c3e1e92f8250d4cfbb30632296140c2d6dbef09b0fa"}, + {file = "faker-39.0.0-py3-none-any.whl", hash = "sha256:c72f1fca8f1a24b8da10fcaa45739135a19772218ddd61b86b7ea1b8c790dce7"}, + {file = "faker-39.0.0.tar.gz", hash = "sha256:ddae46d3b27e01cea7894651d687b33bcbe19a45ef044042c721ceac6d3da0ff"}, ] [package.dependencies] @@ -479,37 +430,37 @@ tzdata = "*" [[package]] name = "fastapi" -version = "0.120.4" +version = "0.127.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "fastapi-0.120.4-py3-none-any.whl", hash = "sha256:9bdf192308676480d3593e10fd05094e56d6fdc7d9283db26053d8104d5f82a0"}, - {file = "fastapi-0.120.4.tar.gz", hash = "sha256:2d856bc847893ca4d77896d4504ffdec0fb04312b705065fca9104428eca3868"}, + {file = "fastapi-0.127.0-py3-none-any.whl", hash = "sha256:725aa2bb904e2eff8031557cf4b9b77459bfedd63cae8427634744fd199f6a49"}, + {file = "fastapi-0.127.0.tar.gz", hash = "sha256:5a9246e03dcd1fdb19f1396db30894867c1d630f5107dc167dcbc5ed1ea7d259"}, ] [package.dependencies] annotated-doc = ">=0.0.2" -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.50.0" +pydantic = ">=2.7.0" +starlette = ">=0.40.0,<0.51.0" typing-extensions = ">=4.8.0" [package.extras] all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] -standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "genbadge" -version = "1.1.2" +version = "1.1.3" description = "Generate badges for tools that do not provide one." optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "genbadge-1.1.2-py2.py3-none-any.whl", hash = "sha256:4e3073cb56c2745fbef4b7da97eb85b28a18a22af519b66acb6706b6546279f1"}, - {file = "genbadge-1.1.2.tar.gz", hash = "sha256:987ed2feaf6e9cc2850fc3883320d8706b3849eb6c9f436156254dcac438515c"}, + {file = "genbadge-1.1.3-py2.py3-none-any.whl", hash = "sha256:6e4316c171c6f0f84becae4eb116258340bdc054458632abc622d36b8040655e"}, + {file = "genbadge-1.1.3.tar.gz", hash = "sha256:2292ea9cc20af4463dfde952c6b15544fdab9d6e50945f63a42cc400c521fa74"}, ] [package.dependencies] @@ -719,6 +670,93 @@ files = [ {file = "lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61"}, ] +[[package]] +name = "librt" +version = "0.7.4" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["type-checking"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.7.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc300cb5a5a01947b1ee8099233156fdccd5001739e5f596ecfbc0dab07b5a3b"}, + {file = "librt-0.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee8d3323d921e0f6919918a97f9b5445a7dfe647270b2629ec1008aa676c0bc0"}, + {file = "librt-0.7.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:95cb80854a355b284c55f79674f6187cc9574df4dc362524e0cce98c89ee8331"}, + {file = "librt-0.7.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca1caedf8331d8ad6027f93b52d68ed8f8009f5c420c246a46fe9d3be06be0f"}, + {file = "librt-0.7.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a6f1236151e6fe1da289351b5b5bce49651c91554ecc7b70a947bced6fe212"}, + {file = "librt-0.7.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7766b57aeebaf3f1dac14fdd4a75c9a61f2ed56d8ebeefe4189db1cb9d2a3783"}, + {file = "librt-0.7.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1c4c89fb01157dd0a3bfe9e75cd6253b0a1678922befcd664eca0772a4c6c979"}, + {file = "librt-0.7.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7fa8beef580091c02b4fd26542de046b2abfe0aaefa02e8bcf68acb7618f2b3"}, + {file = "librt-0.7.4-cp310-cp310-win32.whl", hash = "sha256:543c42fa242faae0466fe72d297976f3c710a357a219b1efde3a0539a68a6997"}, + {file = "librt-0.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:25cc40d8eb63f0a7ea4c8f49f524989b9df901969cb860a2bc0e4bad4b8cb8a8"}, + {file = "librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a"}, + {file = "librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729"}, + {file = "librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed"}, + {file = "librt-0.7.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e8f864b521f6cfedb314d171630f827efee08f5c3462bcbc2244ab8e1768cd6"}, + {file = "librt-0.7.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df7c9def4fc619a9c2ab402d73a0c5b53899abe090e0100323b13ccb5a3dd82"}, + {file = "librt-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f79bc3595b6ed159a1bf0cdc70ed6ebec393a874565cab7088a219cca14da727"}, + {file = "librt-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77772a4b8b5f77d47d883846928c36d730b6e612a6388c74cba33ad9eb149c11"}, + {file = "librt-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:064a286e6ab0b4c900e228ab4fa9cb3811b4b83d3e0cc5cd816b2d0f548cb61c"}, + {file = "librt-0.7.4-cp311-cp311-win32.whl", hash = "sha256:42da201c47c77b6cc91fc17e0e2b330154428d35d6024f3278aa2683e7e2daf2"}, + {file = "librt-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:d31acb5886c16ae1711741f22504195af46edec8315fe69b77e477682a87a83e"}, + {file = "librt-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:114722f35093da080a333b3834fff04ef43147577ed99dd4db574b03a5f7d170"}, + {file = "librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92"}, + {file = "librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108"}, + {file = "librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94"}, + {file = "librt-0.7.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce58420e25097b2fc201aef9b9f6d65df1eb8438e51154e1a7feb8847e4a55ab"}, + {file = "librt-0.7.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b719c8730c02a606dc0e8413287e8e94ac2d32a51153b300baf1f62347858fba"}, + {file = "librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9"}, + {file = "librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74"}, + {file = "librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f"}, + {file = "librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286"}, + {file = "librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20"}, + {file = "librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a"}, + {file = "librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b"}, + {file = "librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32"}, + {file = "librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67"}, + {file = "librt-0.7.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b370a77be0a16e1ad0270822c12c21462dc40496e891d3b0caf1617c8cc57e20"}, + {file = "librt-0.7.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d05acd46b9a52087bfc50c59dfdf96a2c480a601e8898a44821c7fd676598f74"}, + {file = "librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee"}, + {file = "librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681"}, + {file = "librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c"}, + {file = "librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2"}, + {file = "librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e"}, + {file = "librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788"}, + {file = "librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d"}, + {file = "librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23"}, + {file = "librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063"}, + {file = "librt-0.7.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39003fc73f925e684f8521b2dbf34f61a5deb8a20a15dcf53e0d823190ce8848"}, + {file = "librt-0.7.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb15ee29d95875ad697d449fe6071b67f730f15a6961913a2b0205015ca0843"}, + {file = "librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a"}, + {file = "librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16"}, + {file = "librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce"}, + {file = "librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f"}, + {file = "librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a"}, + {file = "librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44"}, + {file = "librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105"}, + {file = "librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4"}, + {file = "librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a"}, + {file = "librt-0.7.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c96cb76f055b33308f6858b9b594618f1b46e147a4d03a4d7f0c449e304b9b95"}, + {file = "librt-0.7.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f990e6821204f516d09dc39966ef8b84556ffd648d5926c9a3f681e8de8906"}, + {file = "librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf"}, + {file = "librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f"}, + {file = "librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5"}, + {file = "librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb"}, + {file = "librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481"}, + {file = "librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f"}, + {file = "librt-0.7.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6fc4aa67fedd827a601f97f0e61cc72711d0a9165f2c518e9a7c38fc1568b9ad"}, + {file = "librt-0.7.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e710c983d29d9cc4da29113b323647db286eaf384746344f4a233708cca1a82c"}, + {file = "librt-0.7.4-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:43a2515a33f2bc17b15f7fb49ff6426e49cb1d5b2539bc7f8126b9c5c7f37164"}, + {file = "librt-0.7.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fd766bb9ace3498f6b93d32f30c0e7c8ce6b727fecbc84d28160e217bb66254"}, + {file = "librt-0.7.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce1b44091355b68cffd16e2abac07c1cafa953fa935852d3a4dd8975044ca3bf"}, + {file = "librt-0.7.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5a72b905420c4bb2c10c87b5c09fe6faf4a76d64730e3802feef255e43dfbf5a"}, + {file = "librt-0.7.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07c4d7c9305e75a0edd3427b79c7bd1d019cd7eddaa7c89dbb10e0c7946bffbb"}, + {file = "librt-0.7.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2e734c2c54423c6dcc77f58a8585ba83b9f72e422f9edf09cab1096d4a4bdc82"}, + {file = "librt-0.7.4-cp39-cp39-win32.whl", hash = "sha256:a34ae11315d4e26326aaf04e21ccd8d9b7de983635fba38d73e203a9c8e3fe3d"}, + {file = "librt-0.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:7e4b5ffa1614ad4f32237d739699be444be28de95071bfa4e66a8da9fa777798"}, + {file = "librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba"}, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -880,53 +918,54 @@ files = [ [[package]] name = "mypy" -version = "1.18.2" +version = "1.19.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["type-checking"] files = [ - {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, - {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, - {file = "mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b"}, - {file = "mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66"}, - {file = "mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428"}, - {file = "mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed"}, - {file = "mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f"}, - {file = "mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341"}, - {file = "mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d"}, - {file = "mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86"}, - {file = "mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37"}, - {file = "mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8"}, - {file = "mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34"}, - {file = "mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764"}, - {file = "mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893"}, - {file = "mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914"}, - {file = "mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8"}, - {file = "mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074"}, - {file = "mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc"}, - {file = "mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e"}, - {file = "mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986"}, - {file = "mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d"}, - {file = "mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba"}, - {file = "mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544"}, - {file = "mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce"}, - {file = "mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d"}, - {file = "mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c"}, - {file = "mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb"}, - {file = "mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075"}, - {file = "mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf"}, - {file = "mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b"}, - {file = "mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133"}, - {file = "mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6"}, - {file = "mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac"}, - {file = "mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b"}, - {file = "mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0"}, - {file = "mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e"}, - {file = "mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, ] [package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=0.9.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -945,7 +984,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" -groups = ["main", "type-checking"] +groups = ["type-checking"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -953,47 +992,46 @@ files = [ [[package]] name = "nodeenv" -version = "1.9.1" +version = "1.10.0" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["type-checking"] files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, ] [[package]] name = "openapi-core" -version = "0.19.5" +version = "0.22.0" description = "client-side and server-side support for the OpenAPI Specification v3" optional = false -python-versions = "<4.0.0,>=3.8.0" +python-versions = "<4.0.0,>=3.9.0" groups = ["main"] files = [ - {file = "openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f"}, - {file = "openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3"}, + {file = "openapi_core-0.22.0-py3-none-any.whl", hash = "sha256:8fb7c325f2db4ef6c60584b1870f90eeb3183aa47e30643715c5003b7677a149"}, + {file = "openapi_core-0.22.0.tar.gz", hash = "sha256:b30490dfa74e3aac2276105525590135212352f5dd7e5acf8f62f6a89ed6f2d0"}, ] [package.dependencies] isodate = "*" -jsonschema = ">=4.18.0,<5.0.0" -jsonschema-path = ">=0.3.1,<0.4.0" +jsonschema = ">=4.23.0,<5.0.0" +jsonschema-path = ">=0.3.4,<0.4.0" more-itertools = "*" openapi-schema-validator = ">=0.6.0,<0.7.0" openapi-spec-validator = ">=0.7.1,<0.8.0" -parse = "*" typing-extensions = ">=4.8.0,<5.0.0" -werkzeug = "<3.1.2" +werkzeug = ">=2.1.0" [package.extras] aiohttp = ["aiohttp (>=3.0)", "multidict (>=6.0.4,<7.0.0)"] django = ["django (>=3.0)"] falcon = ["falcon (>=3.0)"] -fastapi = ["fastapi (>=0.111,<0.116)"] +fastapi = ["fastapi (>=0.111,<0.125)"] flask = ["flask"] requests = ["requests"] -starlette = ["aioitertools (>=0.11,<0.13)", "starlette (>=0.26.1,<0.45.0)"] +starlette = ["aioitertools (>=0.11,<0.14)", "starlette (>=0.26.1,<0.50.0)"] [[package]] name = "openapi-schema-validator" @@ -1042,18 +1080,6 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] -[[package]] -name = "parse" -version = "1.20.2" -description = "parse() is the opposite of format()" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, - {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, -] - [[package]] name = "pathable" version = "0.4.4" @@ -1072,7 +1098,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" -groups = ["main", "lint-and-format", "type-checking"] +groups = ["lint-and-format", "type-checking"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1189,14 +1215,14 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.5.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" -groups = ["main", "dev", "lint-and-format", "type-checking"] +groups = ["dev", "lint-and-format", "type-checking"] files = [ - {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, - {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, + {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, + {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, ] [package.extras] @@ -1249,19 +1275,19 @@ ssv = ["swagger-spec-validator (>=3.0.4,<3.1.0)"] [[package]] name = "pydantic" -version = "2.12.3" +version = "2.12.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf"}, - {file = "pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74"}, + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.41.4" +pydantic-core = "2.41.5" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -1271,129 +1297,133 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.41.4" +version = "2.41.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e"}, - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:646e76293345954acea6966149683047b7b2ace793011922208c8e9da12b0062"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc8e85a63085a137d286e2791037f5fdfff0aabb8b899483ca9c496dd5797338"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c622c8f859a17c156492783902d8370ac7e121a611bd6fe92cc71acf9ee8d"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1e2906efb1031a532600679b424ef1d95d9f9fb507f813951f23320903adbd7"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04e2f7f8916ad3ddd417a7abdd295276a0bf216993d9318a5d61cc058209166"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df649916b81822543d1c8e0e1d079235f68acdc7d270c911e8425045a8cfc57e"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c529f862fdba70558061bb936fe00ddbaaa0c647fd26e4a4356ef1d6561891"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3b4c5a1fd3a311563ed866c2c9b62da06cb6398bee186484ce95c820db71cb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e0fc40d84448f941df9b3334c4b78fe42f36e3bf631ad54c3047a0cdddc2514"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:44e7625332683b6c1c8b980461475cde9595eff94447500e80716db89b0da005"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:170ee6835f6c71081d031ef1c3b4dc4a12b9efa6a9540f93f95b82f3c7571ae8"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win32.whl", hash = "sha256:3adf61415efa6ce977041ba9745183c0e1f637ca849773afa93833e04b163feb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win_amd64.whl", hash = "sha256:a238dd3feee263eeaeb7dc44aea4ba1364682c4f9f9467e6af5596ba322c2332"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f"}, - {file = "pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, ] [package.dependencies] @@ -1416,18 +1446,18 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "4.0.2" +version = "4.0.4" description = "python code static checker" optional = false python-versions = ">=3.10.0" groups = ["lint-and-format"] files = [ - {file = "pylint-4.0.2-py3-none-any.whl", hash = "sha256:9627ccd129893fb8ee8e8010261cb13485daca83e61a6f854a85528ee579502d"}, - {file = "pylint-4.0.2.tar.gz", hash = "sha256:9c22dfa52781d3b79ce86ab2463940f874921a3e5707bcfc98dd0c019945014e"}, + {file = "pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0"}, + {file = "pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2"}, ] [package.dependencies] -astroid = ">=4.0.1,<=4.1.dev0" +astroid = ">=4.0.2,<=4.1.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -1465,21 +1495,6 @@ all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] nodejs = ["nodejs-wheel-binaries"] -[[package]] -name = "pytokens" -version = "0.2.0" -description = "A Fast, spec compliant Python 3.13+ tokenizer that runs on older Pythons." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8"}, - {file = "pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43"}, -] - -[package.extras] -dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] - [[package]] name = "pytz" version = "2025.2" @@ -1650,14 +1665,14 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rich-click" -version = "1.9.4" +version = "1.9.5" description = "Format click help output nicely with rich" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "rich_click-1.9.4-py3-none-any.whl", hash = "sha256:d70f39938bcecaf5543e8750828cbea94ef51853f7d0e174cda1e10543767389"}, - {file = "rich_click-1.9.4.tar.gz", hash = "sha256:af73dc68e85f3bebb80ce302a642b9fe3b65f3df0ceb42eb9a27c467c1b678c8"}, + {file = "rich_click-1.9.5-py3-none-any.whl", hash = "sha256:9b195721a773b1acf0e16ff9ec68cef1e7d237e53471e6e3f7ade462f86c403a"}, + {file = "rich_click-1.9.5.tar.gz", hash = "sha256:48120531493f1533828da80e13e839d471979ec8d7d0ca7b35f86a1379cc74b6"}, ] [package.dependencies] @@ -1672,62 +1687,62 @@ docs = ["markdown-include (>=0.8.1)", "mike (>=2.1.3)", "mkdocs-github-admonitio [[package]] name = "robotcode" -version = "2.0.1" +version = "2.1.0" description = "Command line interface for RobotCode" optional = false python-versions = ">=3.10" groups = ["dev", "type-checking"] files = [ - {file = "robotcode-2.0.1-py3-none-any.whl", hash = "sha256:4259fc1a3b261c7d01194c019fd7c9cd800c93a0d18a5d3b45d5bd21293ac913"}, - {file = "robotcode-2.0.1.tar.gz", hash = "sha256:2fe0509d91e2f7e351c24bc83cb493ef443ca12c32b0b0a1499309015dd544d1"}, + {file = "robotcode-2.1.0-py3-none-any.whl", hash = "sha256:5e10b76b445b9818b92e5e242b58e1fbdfdc2c3c0ebf9e692b9e999211d66ba7"}, + {file = "robotcode-2.1.0.tar.gz", hash = "sha256:c476dfac87b11ecc9223bd6a14790c7211bef7896ef62125bc8ff58ebd2b208f"}, ] [package.dependencies] -robotcode-core = "2.0.1" -robotcode-plugin = "2.0.1" -robotcode-robot = "2.0.1" +robotcode-core = "*" +robotcode-plugin = "*" +robotcode-robot = "*" [package.extras] -all = ["docutils", "pyyaml (>=5.4)", "rich", "robotcode-analyze (==2.0.1)", "robotcode-debugger (==2.0.1)", "robotcode-language-server (==2.0.1)", "robotcode-repl (==2.0.1)", "robotcode-repl-server (==2.0.1)", "robotcode-runner (==2.0.1)", "robotframework-robocop (>=2.0.0)"] -analyze = ["robotcode-analyze (==2.0.1)"] +all = ["docutils", "pyyaml (>=5.4)", "rich", "robotcode-analyze", "robotcode-debugger", "robotcode-language-server", "robotcode-repl", "robotcode-repl-server", "robotcode-runner", "robotframework-robocop (>=6.0.0)"] +analyze = ["robotcode-analyze (==2.1.0)"] colored = ["rich"] -debugger = ["robotcode-debugger (==2.0.1)"] -languageserver = ["robotcode-language-server (==2.0.1)"] +debugger = ["robotcode-debugger (==2.1.0)"] +languageserver = ["robotcode-language-server (==2.1.0)"] lint = ["robotframework-robocop (>=2.0.0)"] -repl = ["robotcode-repl (==2.0.1)"] -replserver = ["robotcode-repl-server (==2.0.1)"] +repl = ["robotcode-repl (==2.1.0)"] +replserver = ["robotcode-repl-server (==2.1.0)"] rest = ["docutils"] -runner = ["robotcode-runner (==2.0.1)"] +runner = ["robotcode-runner (==2.1.0)"] yaml = ["pyyaml (>=5.4)"] [[package]] name = "robotcode-analyze" -version = "2.0.1" +version = "2.1.0" description = "RobotCode analyze plugin for Robot Framework" optional = false python-versions = ">=3.10" groups = ["type-checking"] files = [ - {file = "robotcode_analyze-2.0.1-py3-none-any.whl", hash = "sha256:b6589fb93b90d82b8506301833157bd243bded858cf2d890b78387bc9ca9d5bf"}, - {file = "robotcode_analyze-2.0.1.tar.gz", hash = "sha256:4e57805e8ee79f8fb5c210c15f39e817ed109200e53d26c9fa81dd9474a87eab"}, + {file = "robotcode_analyze-2.1.0-py3-none-any.whl", hash = "sha256:08eba0935c01c8ccb74b193043463c114c392363dd2c37dbf79f0fc44909ffe6"}, + {file = "robotcode_analyze-2.1.0.tar.gz", hash = "sha256:9d6e29c67302f38a92963c85d9523f72bbd1cae6494658cee283c02c4095e333"}, ] [package.dependencies] -robotcode = "2.0.1" -robotcode-plugin = "2.0.1" -robotcode-robot = "2.0.1" -robotframework = ">=4.1.0" +robotcode = "*" +robotcode-plugin = "*" +robotcode-robot = "*" +robotframework = ">=5.0.0" [[package]] name = "robotcode-core" -version = "2.0.1" +version = "2.1.0" description = "Some core classes for RobotCode" optional = false python-versions = ">=3.10" groups = ["dev", "type-checking"] files = [ - {file = "robotcode_core-2.0.1-py3-none-any.whl", hash = "sha256:0f77be39d42ad4e331e5b7e19809fc631fb046b1424426d3f582ac2a83eb127a"}, - {file = "robotcode_core-2.0.1.tar.gz", hash = "sha256:0e5240064f057ff9e64641e896ed16c23c134ecdf9b5116a4c574a5477e4e679"}, + {file = "robotcode_core-2.1.0-py3-none-any.whl", hash = "sha256:fc6603553adef020fe43db962dc5145d60aee3983ede9b587cf9b2ce54ac21b9"}, + {file = "robotcode_core-2.1.0.tar.gz", hash = "sha256:e8c100e037d72dde2da737f6e4ab0b0084e6db4205fe4dd852d63213dd319ff3"}, ] [package.dependencies] @@ -1735,29 +1750,29 @@ typing-extensions = ">=4.4.0" [[package]] name = "robotcode-modifiers" -version = "2.0.1" +version = "2.1.0" description = "Some Robot Framework Modifiers for RobotCode" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "robotcode_modifiers-2.0.1-py3-none-any.whl", hash = "sha256:827dc9c1ed4f63b6ebce6be67a105d2030cc18266dd4cf8de14f5411ea968678"}, - {file = "robotcode_modifiers-2.0.1.tar.gz", hash = "sha256:ce6159925d86360a1f205d4a45a57c7facf7fd3b3c95a1a966805a66b5dd7fab"}, + {file = "robotcode_modifiers-2.1.0-py3-none-any.whl", hash = "sha256:2b941b7c21d96c0cbe060f3e88dda57ab72ddbb88b1951acf6d2fbfbb4b0b078"}, + {file = "robotcode_modifiers-2.1.0.tar.gz", hash = "sha256:a31a6be68ab6c6dd02f7a3c6a86dc1c4381c9cb9e8bc40146a562f59187f2c7e"}, ] [package.dependencies] -robotframework = ">=4.1.0" +robotframework = ">=5.0.0" [[package]] name = "robotcode-plugin" -version = "2.0.1" +version = "2.1.0" description = "Some classes for RobotCode plugin management" optional = false python-versions = ">=3.10" groups = ["dev", "type-checking"] files = [ - {file = "robotcode_plugin-2.0.1-py3-none-any.whl", hash = "sha256:85e239edfd8c4b6e28af2ee9139958eaef9f09e0aef395f2ed2bd2d5593db4f6"}, - {file = "robotcode_plugin-2.0.1.tar.gz", hash = "sha256:ceb89b663f8e017b1b20bf770113ab51074cf06516187f695e9917e8a84279b4"}, + {file = "robotcode_plugin-2.1.0-py3-none-any.whl", hash = "sha256:140323c767ef96c34527807176eff0dd67e8b1680fb8086886596cab5a17f0fe"}, + {file = "robotcode_plugin-2.1.0.tar.gz", hash = "sha256:8bfc2eb78ec72c78f0bad7efa9eebb7345a121abc75a24882d1f2af80f14b838"}, ] [package.dependencies] @@ -1768,51 +1783,51 @@ tomli-w = ">=1.0.0" [[package]] name = "robotcode-robot" -version = "2.0.1" +version = "2.1.0" description = "Support classes for RobotCode for handling Robot Framework projects." optional = false python-versions = ">=3.10" groups = ["dev", "type-checking"] files = [ - {file = "robotcode_robot-2.0.1-py3-none-any.whl", hash = "sha256:628cafed3525a28928f95ae7f4a79dedd3983b042b443a8e0d10380cc8617b85"}, - {file = "robotcode_robot-2.0.1.tar.gz", hash = "sha256:bb7bba064bd4dbda240975fb9093388926daef5cc7faaa14ed366f89fc38f543"}, + {file = "robotcode_robot-2.1.0-py3-none-any.whl", hash = "sha256:5d07364576119fd5c580ddf0ee12eb42b661e9ecaf07c9b030ae44de38b01c36"}, + {file = "robotcode_robot-2.1.0.tar.gz", hash = "sha256:048389b8f81677b5cb9356d6fa1ea68f4254e984efc17c8409737d6a53d2cb62"}, ] [package.dependencies] platformdirs = ">=4.3" -robotcode-core = "2.0.1" -robotframework = ">=4.1.0" +robotcode-core = "*" +robotframework = ">=5.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "robotcode-runner" -version = "2.0.1" +version = "2.1.0" description = "RobotCode runner for Robot Framework" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "robotcode_runner-2.0.1-py3-none-any.whl", hash = "sha256:7df0b67fa647fa75bea651c7caf1be6128d54836e5f7cc12d3117f77bd10d644"}, - {file = "robotcode_runner-2.0.1.tar.gz", hash = "sha256:aa350a2b0fe19d32c6efde223e4967d206f63ba085b6c444e914c7855dd15379"}, + {file = "robotcode_runner-2.1.0-py3-none-any.whl", hash = "sha256:02287c14e15d4e8b38409522eabf0c67dcc004b83aac7d8e8f55125eec78297c"}, + {file = "robotcode_runner-2.1.0.tar.gz", hash = "sha256:c22bbb1ec842d741673332e87777c2458975324d3960eb86df9cb7a498952f72"}, ] [package.dependencies] -robotcode = "2.0.1" -robotcode-modifiers = "2.0.1" -robotcode-plugin = "2.0.1" -robotcode-robot = "2.0.1" -robotframework = ">=4.1.0" +robotcode = "*" +robotcode-modifiers = "*" +robotcode-plugin = "*" +robotcode-robot = "*" +robotframework = ">=5.0.0" [[package]] name = "robotframework" -version = "7.3.2" +version = "7.4.1" description = "Generic automation framework for acceptance testing and robotic process automation (RPA)" optional = false python-versions = ">=3.8" groups = ["main", "dev", "lint-and-format", "type-checking"] files = [ - {file = "robotframework-7.3.2-py3-none-any.whl", hash = "sha256:14ef2afa905285cc073df6ce06d0cd3af4a113df6f815532718079e00c98cca4"}, - {file = "robotframework-7.3.2.tar.gz", hash = "sha256:3bb3e299831ecb1664f3d5082f6ff9f08ba82d61a745bef2227328ef3049e93a"}, + {file = "robotframework-7.4.1-py3-none-any.whl", hash = "sha256:3f75cae8a24797f1953efdb51399569767d742bb31f549ee9856db5545062728"}, + {file = "robotframework-7.4.1.tar.gz", hash = "sha256:d645487248a86db1e1a865ce792502792edf5342873f4e4f35d333219cd246c5"}, ] [[package]] @@ -1837,14 +1852,14 @@ xls = ["openpyxl", "pandas", "xlrd (>=1.2.0)"] [[package]] name = "robotframework-robocop" -version = "6.9.2" +version = "7.0.0" description = "Static code analysis tool (linter) and code formatter for Robot Framework" optional = false python-versions = ">=3.9" groups = ["lint-and-format"] files = [ - {file = "robotframework_robocop-6.9.2-py3-none-any.whl", hash = "sha256:1b6111c614cce67af33998aa35cac60ccc8a1e495b0be44b6b8892a7cdcc7cf9"}, - {file = "robotframework_robocop-6.9.2.tar.gz", hash = "sha256:461b1ae8ad9a43ae1a29ba343ec9b626c65cd8615938e94b76c3f32c0eee39f6"}, + {file = "robotframework_robocop-7.0.0-py3-none-any.whl", hash = "sha256:966cc703183fc562c6f8b75a3342cd005bd0a54da0e4172fab8ee3e3f70d34c2"}, + {file = "robotframework_robocop-7.0.0.tar.gz", hash = "sha256:1533c613c69f9e264b051da473aab018fa03f509ad51bd441b54ca5cd7a10fea"}, ] [package.dependencies] @@ -1875,127 +1890,127 @@ robotframework = ">=3.2" [[package]] name = "rpds-py" -version = "0.28.0" +version = "0.30.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "rpds_py-0.28.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7b6013db815417eeb56b2d9d7324e64fcd4fa289caeee6e7a78b2e11fc9b438a"}, - {file = "rpds_py-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a4c6b05c685c0c03f80dabaeb73e74218c49deea965ca63f76a752807397207"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4794c6c3fbe8f9ac87699b131a1f26e7b4abcf6d828da46a3a52648c7930eba"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e8456b6ee5527112ff2354dd9087b030e3429e43a74f480d4a5ca79d269fd85"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:beb880a9ca0a117415f241f66d56025c02037f7c4efc6fe59b5b8454f1eaa50d"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6897bebb118c44b38c9cb62a178e09f1593c949391b9a1a6fe777ccab5934ee7"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b553dd06e875249fd43efd727785efb57a53180e0fde321468222eabbeaafa"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:f0b2044fdddeea5b05df832e50d2a06fe61023acb44d76978e1b060206a8a476"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05cf1e74900e8da73fa08cc76c74a03345e5a3e37691d07cfe2092d7d8e27b04"}, - {file = "rpds_py-0.28.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:efd489fec7c311dae25e94fe7eeda4b3d06be71c68f2cf2e8ef990ffcd2cd7e8"}, - {file = "rpds_py-0.28.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada7754a10faacd4f26067e62de52d6af93b6d9542f0df73c57b9771eb3ba9c4"}, - {file = "rpds_py-0.28.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c2a34fd26588949e1e7977cfcbb17a9a42c948c100cab890c6d8d823f0586457"}, - {file = "rpds_py-0.28.0-cp310-cp310-win32.whl", hash = "sha256:f9174471d6920cbc5e82a7822de8dfd4dcea86eb828b04fc8c6519a77b0ee51e"}, - {file = "rpds_py-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:6e32dd207e2c4f8475257a3540ab8a93eff997abfa0a3fdb287cae0d6cd874b8"}, - {file = "rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296"}, - {file = "rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e"}, - {file = "rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67"}, - {file = "rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d"}, - {file = "rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6"}, - {file = "rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c"}, - {file = "rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa"}, - {file = "rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120"}, - {file = "rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f"}, - {file = "rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28"}, - {file = "rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a"}, - {file = "rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5"}, - {file = "rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c"}, - {file = "rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08"}, - {file = "rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c"}, - {file = "rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd"}, - {file = "rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b"}, - {file = "rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb"}, - {file = "rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41"}, - {file = "rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7"}, - {file = "rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9"}, - {file = "rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5"}, - {file = "rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e"}, - {file = "rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1"}, - {file = "rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c"}, - {file = "rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a"}, - {file = "rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f"}, - {file = "rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37"}, - {file = "rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712"}, - {file = "rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342"}, - {file = "rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907"}, - {file = "rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472"}, - {file = "rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728"}, - {file = "rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01"}, - {file = "rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515"}, - {file = "rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e"}, - {file = "rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f"}, - {file = "rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1"}, - {file = "rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d"}, - {file = "rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b"}, - {file = "rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e"}, - {file = "rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1"}, - {file = "rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c"}, - {file = "rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092"}, - {file = "rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3"}, - {file = "rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f"}, - {file = "rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, ] [[package]] @@ -2012,18 +2027,18 @@ files = [ [[package]] name = "ruamel-yaml" -version = "0.18.16" +version = "0.18.17" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "ruamel.yaml-0.18.16-py3-none-any.whl", hash = "sha256:048f26d64245bae57a4f9ef6feb5b552a386830ef7a826f235ffb804c59efbba"}, - {file = "ruamel.yaml-0.18.16.tar.gz", hash = "sha256:a6e587512f3c998b2225d68aa1f35111c29fad14aed561a26e73fab729ec5e5a"}, + {file = "ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d"}, + {file = "ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c"}, ] [package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} +"ruamel.yaml.clib" = {version = ">=0.2.15", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.15\""} [package.extras] docs = ["mercurial (>5.7)", "ryd"] @@ -2031,97 +2046,103 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [[package]] name = "ruamel-yaml-clib" -version = "0.2.14" +version = "0.2.15" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version < \"3.14\" and platform_python_implementation == \"CPython\"" -files = [ - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f8b2acb0ffdd2ce8208accbec2dca4a06937d556fdcaefd6473ba1b5daa7e3c4"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:aef953f3b8bd0b50bd52a2e52fb54a6a2171a1889d8dea4a5959d46c6624c451"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a0ac90efbc7a77b0d796c03c8cc4e62fd710b3f1e4c32947713ef2ef52e09543"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bf6b699223afe6c7fe9f2ef76e0bfa6dd892c21e94ce8c957478987ade76cd8"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d73a0187718f6eec5b2f729b0f98e4603f7bd9c48aa65d01227d1a5dcdfbe9e8"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81f6d3b19bc703679a5705c6a16dabdc79823c71d791d73c65949be7f3012c02"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b28caeaf3e670c08cb7e8de221266df8494c169bd6ed8875493fab45be9607a4"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94f3efb718f8f49b031f2071ec7a27dd20cbfe511b4dfd54ecee54c956da2b31"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-win32.whl", hash = "sha256:27c070cf3888e90d992be75dd47292ff9aa17dafd36492812a6a304a1aedc182"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:4f4a150a737fccae13fb51234d41304ff2222e3b7d4c8e9428ed1a6ab48389b8"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5bae1a073ca4244620425cd3d3aa9746bde590992b98ee8c7c8be8c597ca0d4e"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:0a54e5e40a7a691a426c2703b09b0d61a14294d25cfacc00631aa6f9c964df0d"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:10d9595b6a19778f3269399eff6bab642608e5966183abc2adbe558a42d4efc9"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba72975485f2b87b786075e18a6e5d07dc2b4d8973beb2732b9b2816f1bad70"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29757bdb7c142f9595cc1b62ec49a3d1c83fab9cef92db52b0ccebaad4eafb98"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:557df28dbccf79b152fe2d1b935f6063d9cc431199ea2b0e84892f35c03bb0ee"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:26a8de280ab0d22b6e3ec745b4a5a07151a0f74aad92dd76ab9c8d8d7087720d"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e501c096aa3889133d674605ebd018471bc404a59cbc17da3c5924421c54d97c"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-win32.whl", hash = "sha256:915748cfc25b8cfd81b14d00f4bfdb2ab227a30d6d43459034533f4d1c207a2a"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:4ccba93c1e5a40af45b2f08e4591969fa4697eae951c708f3f83dcbf9f6c6bb1"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6aeadc170090ff1889f0d2c3057557f9cd71f975f17535c26a5d37af98f19c27"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5e56ac47260c0eed992789fa0b8efe43404a9adb608608631a948cee4fc2b052"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a911aa73588d9a8b08d662b9484bc0567949529824a55d3885b77e8dd62a127a"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05ba88adf3d7189a974b2de7a9d56731548d35dc0a822ec3dc669caa7019b29"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb04c5650de6668b853623eceadcdb1a9f2fee381f5d7b6bc842ee7c239eeec4"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df3ec9959241d07bc261f4983d25a1205ff37703faf42b474f15d54d88b4f8c9"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fbc08c02e9b147a11dfcaa1ac8a83168b699863493e183f7c0c8b12850b7d259"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c099cafc1834d3c5dac305865d04235f7c21c167c8dd31ebc3d6bbc357e2f023"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-win32.whl", hash = "sha256:b5b0f7e294700b615a3bcf6d28b26e6da94e8eba63b079f4ec92e9ba6c0d6b54"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:a37f40a859b503304dd740686359fcf541d6fb3ff7fc10f539af7f7150917c68"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7e4f9da7e7549946e02a6122dcad00b7c1168513acb1f8a726b1aaf504a99d32"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:dd7546c851e59c06197a7c651335755e74aa383a835878ca86d2c650c07a2f85"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:1c1acc3a0209ea9042cc3cfc0790edd2eddd431a2ec3f8283d081e4d5018571e"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2070bf0ad1540d5c77a664de07ebcc45eebd1ddcab71a7a06f26936920692beb"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd8fe07f49c170e09d76773fb86ad9135e0beee44f36e1576a201b0676d3d1d"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ff86876889ea478b1381089e55cf9e345707b312beda4986f823e1d95e8c0f59"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1f118b707eece8cf84ecbc3e3ec94d9db879d85ed608f95870d39b2d2efa5dca"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b30110b29484adc597df6bd92a37b90e63a8c152ca8136aad100a02f8ba6d1b6"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-win32.whl", hash = "sha256:f4e97a1cf0b7a30af9e1d9dad10a5671157b9acee790d9e26996391f49b965a2"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:090782b5fb9d98df96509eecdbcaffd037d47389a89492320280d52f91330d78"}, - {file = "ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7df6f6e9d0e33c7b1d435defb185095386c469109de723d514142632a7b9d07f"}, - {file = "ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83"}, - {file = "ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27"}, - {file = "ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:18c041b28f3456ddef1f1951d4492dbebe0f8114157c1b3c981a4611c2020792"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:d8354515ab62f95a07deaf7f845886cc50e2f345ceab240a3d2d09a9f7d77853"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:275f938692013a3883edbd848edde6d9f26825d65c9a2eb1db8baa1adc96a05d"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a60d69f4057ad9a92f3444e2367c08490daed6428291aa16cefb445c29b0e9"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ac5ff9425d8acb8f59ac5b96bcb7fd3d272dc92d96a7c730025928ffcc88a7a"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e1d1735d97fd8a48473af048739379975651fab186f8a25a9f683534e6904179"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:83bbd8354f6abb3fdfb922d1ed47ad8d1db3ea72b0523dac8d07cdacfe1c0fcf"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:808c7190a0fe7ae7014c42f73897cf8e9ef14ff3aa533450e51b1e72ec5239ad"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-win32.whl", hash = "sha256:6d5472f63a31b042aadf5ed28dd3ef0523da49ac17f0463e10fda9c4a2773352"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-win_amd64.whl", hash = "sha256:8dd3c2cc49caa7a8d64b67146462aed6723a0495e44bf0aa0a2e94beaa8432f6"}, - {file = "ruamel.yaml.clib-0.2.14.tar.gz", hash = "sha256:803f5044b13602d58ea378576dd75aa759f52116a0232608e8fdada4da33752e"}, +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.15\"" +files = [ + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03"}, + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77"}, + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614"}, + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3"}, + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862"}, + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d"}, + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6"}, + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed"}, + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-win32.whl", hash = "sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f"}, + {file = "ruamel_yaml_clib-0.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:923816815974425fbb1f1bf57e85eca6e14d8adc313c66db21c094927ad01815"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dcc7f3162d3711fd5d52e2267e44636e3e566d1e5675a5f0b30e98f2c4af7974"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d3c9210219cbc0f22706f19b154c9a798ff65a6beeafbf77fc9c057ec806f7d"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bb7b728fd9f405aa00b4a0b17ba3f3b810d0ccc5f77f7373162e9b5f0ff75d5"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cb75a3c14f1d6c3c2a94631e362802f70e83e20d1f2b2ef3026c05b415c4900"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:badd1d7283f3e5894779a6ea8944cc765138b96804496c91812b2829f70e18a7"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0ba6604bbc3dfcef844631932d06a1a4dcac3fee904efccf582261948431628a"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8220fd4c6f98485e97aea65e1df76d4fed1678ede1fe1d0eed2957230d287c4"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-win32.whl", hash = "sha256:04d21dc9c57d9608225da28285900762befbb0165ae48482c15d8d4989d4af14"}, + {file = "ruamel_yaml_clib-0.2.15-cp39-cp39-win_amd64.whl", hash = "sha256:27dc656e84396e6d687f97c6e65fb284d100483628f02d95464fd731743a4afe"}, + {file = "ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600"}, ] [[package]] name = "ruff" -version = "0.14.3" +version = "0.14.10" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["lint-and-format"] files = [ - {file = "ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371"}, - {file = "ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654"}, - {file = "ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5"}, - {file = "ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e"}, - {file = "ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e"}, - {file = "ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa"}, - {file = "ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f"}, - {file = "ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7"}, - {file = "ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f"}, - {file = "ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1"}, - {file = "ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153"}, + {file = "ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49"}, + {file = "ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f"}, + {file = "ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f"}, + {file = "ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d"}, + {file = "ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405"}, + {file = "ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60"}, + {file = "ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830"}, + {file = "ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6"}, + {file = "ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154"}, + {file = "ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6"}, + {file = "ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4"}, ] [[package]] @@ -2157,28 +2178,16 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "starlette" -version = "0.49.3" +version = "0.50.0" description = "The little ASGI library that shines." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f"}, - {file = "starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284"}, + {file = "starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca"}, + {file = "starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca"}, ] [package.dependencies] @@ -2194,8 +2203,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["main", "dev", "lint-and-format", "type-checking"] -markers = "python_version == \"3.10\"" +groups = ["dev", "lint-and-format", "type-checking"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2257,14 +2266,14 @@ files = [ [[package]] name = "typer-slim" -version = "0.20.0" +version = "0.20.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.8" groups = ["lint-and-format"] files = [ - {file = "typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d"}, - {file = "typer_slim-0.20.0.tar.gz", hash = "sha256:9fc6607b3c6c20f5c33ea9590cbeb17848667c51feee27d9e314a579ab07d1a3"}, + {file = "typer_slim-0.20.1-py3-none-any.whl", hash = "sha256:8e89c5dbaffe87a4f86f4c7a9e2f7059b5b68c66f558f298969d42ce34f10122"}, + {file = "typer_slim-0.20.1.tar.gz", hash = "sha256:bb9e4f7e6dc31551c8a201383df322b81b0ce37239a5ead302598a2ebb6f7c9c"}, ] [package.dependencies] @@ -2330,44 +2339,44 @@ typing-extensions = ">=4.12.0" [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, + {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, + {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, ] [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["main", "dev", "type-checking"] files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, + {file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"}, + {file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "uvicorn" -version = "0.38.0" +version = "0.40.0" description = "The lightning-fast ASGI server." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02"}, - {file = "uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d"}, + {file = "uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee"}, + {file = "uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea"}, ] [package.dependencies] @@ -2380,18 +2389,18 @@ standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3) [[package]] name = "werkzeug" -version = "3.1.1" +version = "3.1.4" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5"}, - {file = "werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4"}, + {file = "werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905"}, + {file = "werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e"}, ] [package.dependencies] -MarkupSafe = ">=2.1.1" +markupsafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] @@ -2399,4 +2408,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" python-versions = ">=3.10, <4" -content-hash = "b7ea95a1a29ef5ce4aec7eb44e253601508ae6b67b5d1091db3e5dc0d4252e8c" +content-hash = "1007af3ad0f6fd0278abe498547d74e90b5870a522754706c50f85673d11f88b" diff --git a/pyproject.toml b/pyproject.toml index 121ba48..0b3c0fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name="robotframework-openapitools" -version = "1.0.5" +version = "2.0.0b1" description = "A set of Robot Framework libraries to test APIs for which the OAS is available." authors = [ {name = "Robin Mackaij", email = "r.a.mackaij@gmail.com"}, @@ -28,12 +28,11 @@ dependencies = [ "robotframework >= 6.0.0, !=7.0.0", "robotframework-datadriver >= 1.10.0", "requests >= 2.31.0", - "prance[CLI] >= 23", - "Faker >= 23.1.0", + "prance[CLI] >= 25", + "Faker >= 38.0.0", "rstr >= 3.2.0", "openapi-core >= 0.19.0", "rich_click >= 1.7.0", - "black >= 24.1.0", "Jinja2 >= 3.1.2", "pydantic >= 2.11.0", ] @@ -42,23 +41,23 @@ dependencies = [ dev = [ "invoke >= 2.2.0", "robotframework-stacktrace >= 0.4.0", - "uvicorn >= 0.27.0", - "fastapi >= 0.109.0", + "uvicorn >= 0.38.0", + "fastapi >= 0.122.0", "coverage[toml] >= 7.2.0", - "robotcode-runner >= 1.0.3", + "robotcode-runner >= 2.0.0", "genbadge[coverage] >= 1.1.2", ] type-checking = [ "mypy >= 1.14.1", "types-requests >= 2.31.0", "types-invoke >= 2.0.0.0", - "pyright >= 1.1.350", - "robotcode-analyze >= 1.0.3", + "pyright >= 1.1.400", + "robotcode-analyze >= 2.0.0", ] lint-and-format = [ - "ruff >= 0.9.0", - "pylint >= 3.3.3", - "robotframework-robocop >= 5.7.0", + "ruff >= 0.14.0", + "pylint >= 4.0.0", + "robotframework-robocop >= 6.0.0", ] [project.urls] @@ -108,7 +107,7 @@ build-backend = "poetry.core.masonry.api" branch = true parallel = true source = ["src/OpenApiDriver", "src/OpenApiLibCore", "src/openapi_libgen"] -omit = ["src/openapi_libgen/command_line.py"] +omit = ["src/openapi_libgen/command_line.py", "src/OpenApiLibCore/protocols.py"] [tool.coverage.report] exclude_lines = [ @@ -126,6 +125,7 @@ disallow_untyped_defs = true strict = true show_error_codes = true exclude = [] +follow_untyped_imports = true [[tool.mypy.overrides]] module = [ diff --git a/src/OpenApiDriver/__init__.py b/src/OpenApiDriver/__init__.py index 7557256..c3e54a9 100644 --- a/src/OpenApiDriver/__init__.py +++ b/src/OpenApiDriver/__init__.py @@ -6,15 +6,17 @@ - IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint, UniquePropertyValueConstraint: Classes to be subclassed by the library user when implementing a custom mapping module (advanced use). -- Dto, Relation: Base classes that can be used for type annotations. +- RelationsMapping, Relation: Base classes that can be used for type annotations. - IGNORE: A special constant that can be used as a value in the PropertyValueConstraint. """ from importlib.metadata import version from OpenApiDriver.openapidriver import OpenApiDriver -from OpenApiLibCore.dto_base import ( - Dto, +from OpenApiLibCore.data_relations.relations_base import RelationsMapping +from OpenApiLibCore.keyword_logic.validation import ValidationLevel +from OpenApiLibCore.models import IGNORE +from OpenApiLibCore.models.resource_relations import ( IdDependency, IdReference, PathPropertiesConstraint, @@ -22,8 +24,6 @@ ResourceRelation, UniquePropertyValueConstraint, ) -from OpenApiLibCore.validation import ValidationLevel -from OpenApiLibCore.value_utils import IGNORE try: __version__ = version("robotframework-openapidriver") @@ -33,12 +33,12 @@ __all__ = [ "IGNORE", - "Dto", "IdDependency", "IdReference", "OpenApiDriver", "PathPropertiesConstraint", "PropertyValueConstraint", + "RelationsMapping", "ResourceRelation", "UniquePropertyValueConstraint", "ValidationLevel", diff --git a/src/OpenApiDriver/openapi_executors.py b/src/OpenApiDriver/openapi_executors.py index 3daa00d..c56e4cb 100644 --- a/src/OpenApiDriver/openapi_executors.py +++ b/src/OpenApiDriver/openapi_executors.py @@ -6,6 +6,7 @@ from pathlib import Path from random import choice from types import MappingProxyType +from typing import Literal, overload from requests import Response from requests.auth import AuthBase @@ -21,10 +22,10 @@ from OpenApiLibCore import ( OpenApiLibCore, RequestData, - RequestValues, ValidationLevel, ) from OpenApiLibCore.annotations import JSON +from OpenApiLibCore.models.oas_models import ObjectSchema run_keyword = BuiltIn().run_keyword default_str_mapping: Mapping[str, str] = MappingProxyType({}) @@ -38,6 +39,52 @@ ] +@overload +def _run_keyword( + keyword_name: Literal["get_valid_url"], *args: str +) -> str: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_invalidated_url"], *args: str | int +) -> str: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["authorized_request"], *args: object +) -> Response: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_request_data"], *args: str +) -> RequestData: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_invalid_body_data"], *args: object +) -> dict[str, JSON] | list[JSON]: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_invalidated_parameters"], *args: object +) -> tuple[dict[str, JSON], dict[str, str]]: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["validated_request"], *args: object +) -> None: ... # pragma: no cover + + +def _run_keyword(keyword_name: str, *args: object) -> object: + return run_keyword(keyword_name, *args) # pyright: ignore[reportArgumentType] + + @library(scope="SUITE", doc_format="ROBOT") class OpenApiExecutors(OpenApiLibCore): """Main class providing the keywords and core logic to perform endpoint validations.""" @@ -50,7 +97,7 @@ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value response_validation: ValidationLevel = ValidationLevel.WARN, disable_server_validation: bool = True, mappings_path: str | Path = "", - invalid_property_default_response: int = 422, + invalid_data_default_response: int = 422, default_id_property_name: str = "id", faker_locale: str | list[str] = "", require_body_for_invalid_url: bool = False, @@ -74,7 +121,7 @@ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value disable_server_validation=disable_server_validation, mappings_path=mappings_path, default_id_property_name=default_id_property_name, - invalid_property_default_response=invalid_property_default_response, + invalid_data_default_response=invalid_data_default_response, faker_locale=faker_locale, require_body_for_invalid_url=require_body_for_invalid_url, recursion_limit=recursion_limit, @@ -102,7 +149,7 @@ def test_unauthorized(self, path: str, method: str) -> None: > Note: No headers or (json) body are send with the request. For security reasons, the authorization validation should be checked first. """ - url: str = run_keyword("get_valid_url", path) + url = _run_keyword("get_valid_url", path) response = self.session.request( method=method, url=url, @@ -123,8 +170,8 @@ def test_forbidden(self, path: str, method: str) -> None: > Note: No headers or (json) body are send with the request. For security reasons, the access rights validation should be checked first. """ - url: str = run_keyword("get_valid_url", path) - response: Response = run_keyword("authorized_request", url, method) + url = _run_keyword("get_valid_url", path) + response = _run_keyword("authorized_request", url, method) if response.status_code != int(HTTPStatus.FORBIDDEN): raise AssertionError(f"Response {response.status_code} was not 403.") @@ -148,12 +195,10 @@ def test_invalid_url( parameters are send with the request. The `require_body_for_invalid_url` parameter can be set to `True` if needed. """ - valid_url: str = run_keyword("get_valid_url", path) + valid_url = _run_keyword("get_valid_url", path) try: - url = run_keyword( - "get_invalidated_url", valid_url, path, expected_status_code - ) + url = _run_keyword("get_invalidated_url", valid_url, expected_status_code) except Exception as exception: message = getattr(exception, "message", "") if not message.startswith("ValueError"): @@ -166,12 +211,11 @@ def test_invalid_url( params, headers, json_data = None, None, None if self.require_body_for_invalid_url: - request_data: RequestData = run_keyword("get_request_data", path, method) + request_data = _run_keyword("get_request_data", path, method) params = request_data.params headers = request_data.headers - dto = request_data.dto - json_data = dto.as_dict() - response: Response = run_keyword( + json_data = request_data.valid_data + response = _run_keyword( "authorized_request", url, method, params, headers, json_data ) if response.status_code != expected_status_code: @@ -191,68 +235,63 @@ def test_endpoint(self, path: str, method: str, status_code: int) -> None: The keyword calls other keywords to generate the neccesary data to perform the desired operation and validate the response against the openapi document. """ - json_data: dict[str, JSON] = {} original_data = {} - url: str = run_keyword("get_valid_url", path) - request_data: RequestData = run_keyword("get_request_data", path, method) + url = _run_keyword("get_valid_url", path) + request_data = _run_keyword("get_request_data", path, method) params = request_data.params headers = request_data.headers - if request_data.has_body: - json_data = request_data.dto.as_dict() + json_data = request_data.valid_data # when patching, get the original data to check only patched data has changed if method == "PATCH": original_data = self.get_original_data(url=url) # in case of a status code indicating an error, ensure the error occurs if status_code >= int(HTTPStatus.BAD_REQUEST): - invalidation_keyword_data = { - "get_invalid_body_data": [ - "get_invalid_body_data", - url, - method, - status_code, - request_data, - ], - "get_invalidated_parameters": [ - "get_invalidated_parameters", - status_code, - request_data, - ], - } - invalidation_keywords = [] - - if request_data.dto.get_body_relations_for_error_code(status_code): + invalidation_keywords: list[str] = [] + + if request_data.relations_mapping.get_body_relations_for_error_code( + status_code + ): invalidation_keywords.append("get_invalid_body_data") - if request_data.dto.get_parameter_relations_for_error_code(status_code): + if request_data.relations_mapping.get_parameter_relations_for_error_code( + status_code + ): invalidation_keywords.append("get_invalidated_parameters") if invalidation_keywords: - if ( - invalidation_keyword := choice(invalidation_keywords) - ) == "get_invalid_body_data": - json_data = run_keyword( - *invalidation_keyword_data[invalidation_keyword] + invalidation_keyword = choice(invalidation_keywords) + if invalidation_keyword == "get_invalid_body_data": + json_data = _run_keyword( + "get_invalid_body_data", + url, + method, + status_code, + request_data, ) else: - params, headers = run_keyword( - *invalidation_keyword_data[invalidation_keyword] + params, headers = _run_keyword( + "get_invalidated_parameters", status_code, request_data ) # if there are no relations to invalide and the status_code is the default # response_code for invalid properties, invalidate properties instead - elif status_code == self.invalid_property_default_response: + elif status_code == self.invalid_data_default_response: if ( request_data.params_that_can_be_invalidated or request_data.headers_that_can_be_invalidated ): - params, headers = run_keyword( - *invalidation_keyword_data["get_invalidated_parameters"] + params, headers = _run_keyword( + "get_invalidated_parameters", status_code, request_data ) if request_data.body_schema: - json_data = run_keyword( - *invalidation_keyword_data["get_invalid_body_data"] + json_data = _run_keyword( + "get_invalid_body_data", + url, + method, + status_code, + request_data, ) elif request_data.body_schema: - json_data = run_keyword( - *invalidation_keyword_data["get_invalid_body_data"] + json_data = _run_keyword( + "get_invalid_body_data", url, method, status_code, request_data ) else: raise SkipExecution( @@ -260,19 +299,17 @@ def test_endpoint(self, path: str, method: str, status_code: int) -> None: ) else: raise AssertionError( - f"No Dto mapping found to cause status_code {status_code}." + f"No relation found to cause status_code {status_code}." ) - run_keyword( - "perform_validated_request", + _run_keyword( + "validated_request", path, status_code, - RequestValues( - url=url, - method=method, - params=params, - headers=headers, - json_data=json_data, - ), + url, + method, + params, + headers, + json_data, original_data, ) if status_code < int(HTTPStatus.MULTIPLE_CHOICES) and ( @@ -281,27 +318,28 @@ def test_endpoint(self, path: str, method: str, status_code: int) -> None: or request_data.has_optional_headers ): logger.info("Performing request without optional properties and parameters") - url = run_keyword("get_valid_url", path) - request_data = run_keyword("get_request_data", path, method) + url = _run_keyword("get_valid_url", path) + request_data = _run_keyword("get_request_data", path, method) params = request_data.get_required_params() headers = request_data.get_required_headers() - json_data = ( - request_data.get_minimal_body_dict() if request_data.has_body else {} - ) + if isinstance(request_data.body_schema, ObjectSchema): + json_data = ( + request_data.get_minimal_body_dict() + if request_data.has_body + else {} + ) original_data = {} if method == "PATCH": original_data = self.get_original_data(url=url) - run_keyword( - "perform_validated_request", + _run_keyword( + "validated_request", path, status_code, - RequestValues( - url=url, - method=method, - params=params, - headers=headers, - json_data=json_data, - ), + url, + method, + params, + headers, + json_data, original_data, ) @@ -313,10 +351,10 @@ def get_original_data(self, url: str) -> dict[str, JSON]: """ original_data = {} path = self.get_parameterized_path_from_url(url) - get_request_data: RequestData = run_keyword("get_request_data", path, "GET") + get_request_data = _run_keyword("get_request_data", path, "GET") get_params = get_request_data.params get_headers = get_request_data.headers - response: Response = run_keyword( + response = _run_keyword( "authorized_request", url, "GET", get_params, get_headers ) if response.ok: @@ -327,5 +365,5 @@ def get_original_data(self, url: str) -> dict[str, JSON]: def get_keyword_names() -> list[str]: """Curated keywords for libdoc and libspec.""" if getenv("HIDE_INHERITED_KEYWORDS") == "true": - return KEYWORD_NAMES + return KEYWORD_NAMES # pragma: no cover return KEYWORD_NAMES + LIBCORE_KEYWORD_NAMES diff --git a/src/OpenApiDriver/openapi_reader.py b/src/OpenApiDriver/openapi_reader.py index 90be78d..44d8afe 100644 --- a/src/OpenApiDriver/openapi_reader.py +++ b/src/OpenApiDriver/openapi_reader.py @@ -5,7 +5,7 @@ from DataDriver.AbstractReaderClass import AbstractReaderClass from DataDriver.ReaderConfig import TestCaseData -from OpenApiLibCore.models import PathItemObject +from OpenApiLibCore.models.oas_models import PathItemObject class Test: @@ -45,7 +45,7 @@ def get_data_from_source(self) -> list[TestCaseData]: ignored_tests = [Test(*test) for test in getattr(self, "ignored_testcases", [])] for path, path_item in paths.items(): - path_operations = path_item.get_operations() + path_operations = path_item.operations # by reseversing the items, post/put operations come before get and delete for method, operation_data in reversed(path_operations.items()): diff --git a/src/OpenApiDriver/openapidriver.libspec b/src/OpenApiDriver/openapidriver.libspec index ffcf5e6..f4d6fca 100644 --- a/src/OpenApiDriver/openapidriver.libspec +++ b/src/OpenApiDriver/openapidriver.libspec @@ -1,6 +1,6 @@ - -1.0.5 + +2.0.0b1 The OpenApiDriver library provides the keywords and logic for execution of generated test cases based on an OpenAPI document. Visit the <a href="./index.html" target="_blank">OpenApiTools documentation</a> for an introduction. @@ -8,7 +8,7 @@ Visit the <a href="./index.html" target="_blank">OpenApiTools documentatio - + source @@ -73,8 +73,8 @@ Visit the <a href="./index.html" target="_blank">OpenApiTools documentatio - -invalid_property_default_response + +invalid_data_default_response 422 @@ -152,7 +152,7 @@ Visit the <a href="./index.html" target="_blank">OpenApiTools documentatio extra_headers - + @@ -161,11 +161,11 @@ Visit the <a href="./index.html" target="_blank">OpenApiTools documentatio cookies - + - + None @@ -173,7 +173,7 @@ Visit the <a href="./index.html" target="_blank">OpenApiTools documentatio proxies - + @@ -242,7 +242,7 @@ by Response validation. <h3>mappings_path</h3> See the Advanced Use tab for an in-depth explanation. -<h3>invalid_property_default_response</h3> +<h3>invalid_data_default_response</h3> The default response code for requests with a JSON body that does not comply with the schema. Example: a value outside the specified range or a string value @@ -325,7 +325,7 @@ A dictionary of <code>"protocol": "proxy url"</code> to use for all - + path @@ -350,7 +350,7 @@ The keyword calls other keywords to generate the neccesary data to perform the desired operation and validate the response against the openapi document. Validate that performing the `method` operation on `path` results in a `status_code` response. - + path @@ -371,7 +371,7 @@ library should grant insufficient access rights to the target endpoint. reasons, the access rights validation should be checked first. Perform a request for `method` on the `path`, with the provided authorization. - + path @@ -403,7 +403,7 @@ parameters are send with the request. The <span class="name">require_body_ parameter can be set to <span class="name">True</span> if needed. Perform a request for the provided 'path' and 'method' where the url for the `path` is invalidated. - + path @@ -427,7 +427,7 @@ reasons, the authorization validation should be checked first. -<p>Strings <code>TRUE</code>, <code>YES</code>, <code>ON</code> and <code>1</code> are converted to Boolean <code>True</code>, the empty string as well as strings <code>FALSE</code>, <code>NO</code>, <code>OFF</code> and <code>0</code> are converted to Boolean <code>False</code>, and the string <code>NONE</code> is converted to the Python <code>None</code> object. Other strings and other accepted values are passed as-is, allowing keywords to handle them specially if needed. All string comparisons are case-insensitive.</p> +<p>Strings <code>TRUE</code>, <code>YES</code>, <code>ON</code>, <code>1</code> and possible localization specific "true strings" are converted to Boolean <code>True</code>, the empty string, strings <code>FALSE</code>, <code>NO</code>, <code>OFF</code> and <code>0</code> and possibly localization specific "false strings" are converted to Boolean <code>False</code>, and the string <code>NONE</code> is converted to the Python <code>None</code> object. Other strings and all other values are passed as-is, allowing keywords to handle them specially if needed. All string comparisons are case-insensitive.</p> <p>Examples: <code>TRUE</code> (converted to <code>True</code>), <code>off</code> (converted to <code>False</code>), <code>example</code> (used as-is)</p> string @@ -439,22 +439,9 @@ reasons, the authorization validation should be checked first. __init__ - -<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#dict">dictionary</a> literals. They are converted to actual dictionaries using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function. They can contain any values <code>ast.literal_eval</code> supports, including dictionaries and other containers.</p> -<p>If the type has nested types like <code>dict[str, int]</code>, items are converted to those types automatically. This in new in Robot Framework 6.0.</p> -<p>Examples: <code>{'a': 1, 'b': 2}</code>, <code>{'key': 1, 'nested': {'key': 2}}</code></p> - -string -Mapping - - -__init__ - - <p>Conversion is done using Python's <a href="https://docs.python.org/library/functions.html#int">int</a> built-in function. Floating point numbers are accepted only if they can be represented as integers exactly. For example, <code>1.0</code> is accepted and <code>1.1</code> is not.</p> -<p>Starting from RF 4.1, it is possible to use hexadecimal, octal and binary numbers by prefixing values with <code>0x</code>, <code>0o</code> and <code>0b</code>, respectively.</p> -<p>Starting from RF 4.1, spaces and underscores can be used as visual separators for digit grouping purposes.</p> +<p>It is possible to use hexadecimal, octal and binary numbers by prefixing values with <code>0x</code>, <code>0o</code> and <code>0b</code>, respectively. Spaces and underscores can be used as visual separators for digit grouping purposes.</p> <p>Examples: <code>42</code>, <code>-1</code>, <code>0b1010</code>, <code>10 000 000</code>, <code>0xBAD_C0FFEE</code></p> string @@ -467,9 +454,11 @@ reasons, the authorization validation should be checked first. -<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#list">list</a> literals. They are converted to actual lists using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function. They can contain any values <code>ast.literal_eval</code> supports, including lists and other containers.</p> -<p>If the type has nested types like <code>list[int]</code>, items are converted to those types automatically. This in new in Robot Framework 6.0.</p> -<p>Examples: <code>['one', 'two']</code>, <code>[('one', 1), ('two', 2)]</code></p> +<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#list">list</a> or <a href="https://docs.python.org/library/stdtypes.html#tuple">tuple</a> literals. They are converted using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function and possible tuples converted further to lists. They can contain any values <code>ast.literal_eval</code> supports, including lists and other collections.</p> +<p>If the argument is a list, it is used without conversion. Tuples and other sequences are converted to lists.</p> +<p>If the type has nested types like <code>list[int]</code>, items are converted to those types automatically.</p> +<p>Examples: <code>['one', 'two']</code>, <code>[('one', 1), ('two', 2)]</code></p> +<p>Support to convert nested types is new in Robot Framework 6.0. Support for tuple literals is new in Robot Framework 7.4.</p> string Sequence @@ -478,8 +467,22 @@ reasons, the authorization validation should be checked first. __init__ + +<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#dict">dictionary</a> literals. They are converted to actual dictionaries using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function. They can contain any values <code>ast.literal_eval</code> supports, including dictionaries and other collections.</p> +<p>Any mapping is accepted without conversion. An exception is that if the type is <code>MutableMapping</code>, immutable values are converted to <code>dict</code>.</p> +<p>If the type has nested types like <code>Mapping[str, int]</code>, items are converted to those types automatically. This in new in Robot Framework 6.0.</p> +<p>Examples: <code>{'a': 1, 'b': 2}</code>, <code>{'key': 1, 'nested': {'key': 2}}</code></p> + +string +Mapping + + +__init__ + + -<p>String <code>NONE</code> (case-insensitive) is converted to Python <code>None</code> object. Other values cause an error.</p> +<p>String <code>NONE</code> (case-insensitive) and the empty string are converted to the Python <code>None</code> object. Other values cause an error.</p> +<p>Converting the empty string is new in Robot Framework 7.4.</p> string @@ -499,7 +502,9 @@ reasons, the authorization validation should be checked first. -<p>All arguments are converted to Unicode strings.</p> +<p>All arguments are converted to Unicode strings.</p> +<p>Most values are converted simply by using <code>str(value)</code>. An exception is that bytes are mapped directly to Unicode code points with same ordinals. This means that, for example, <code>b"hyv\xe4"</code> becomes <code>"hyvä"</code>.</p> +<p>Converting bytes specially is new Robot Framework 7.4.</p> Any @@ -512,9 +517,11 @@ reasons, the authorization validation should be checked first. -<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#tuple">tuple</a> literals. They are converted to actual tuples using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function. They can contain any values <code>ast.literal_eval</code> supports, including tuples and other containers.</p> -<p>If the type has nested types like <code>tuple[str, int, int]</code>, items are converted to those types automatically. This in new in Robot Framework 6.0.</p> -<p>Examples: <code>('one', 'two')</code>, <code>(('one', 1), ('two', 2))</code></p> +<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#tuple">tuple</a> or <a href="https://docs.python.org/library/stdtypes.html#list">list</a> literals. They are converted using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function and possible lists converted further to tuples. They can contain any values <code>ast.literal_eval</code> supports, including tuples and other collections.</p> +<p>If the argument is a tuple, it is used without conversion. Lists and other sequences are converted to tuples.</p> +<p>If the type has nested types like <code>tuple[str, int, int]</code>, items are converted to those types automatically.</p> +<p>Examples: <code>('one', 'two')</code>, <code>(('one', 1), ('two', 2))</code></p> +<p>Support to convert nested types is new in Robot Framework 6.0. Support for list literals is new in Robot Framework 7.4.</p> string Sequence diff --git a/src/OpenApiDriver/openapidriver.py b/src/OpenApiDriver/openapidriver.py index 5f3026d..ded7560 100644 --- a/src/OpenApiDriver/openapidriver.py +++ b/src/OpenApiDriver/openapidriver.py @@ -34,7 +34,7 @@ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value response_validation: ValidationLevel = ValidationLevel.WARN, disable_server_validation: bool = True, mappings_path: str | Path = "", - invalid_property_default_response: int = 422, + invalid_data_default_response: int = 422, default_id_property_name: str = "id", faker_locale: str | list[str] = "", require_body_for_invalid_url: bool = False, @@ -64,7 +64,7 @@ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value response_validation=response_validation, disable_server_validation=disable_server_validation, mappings_path=mappings_path, - invalid_property_default_response=invalid_property_default_response, + invalid_data_default_response=invalid_data_default_response, default_id_property_name=default_id_property_name, faker_locale=faker_locale, require_body_for_invalid_url=require_body_for_invalid_url, @@ -84,7 +84,7 @@ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value read_paths_method = self.read_paths DataDriver.__init__( self, - reader_class=OpenApiReader, + reader_class=OpenApiReader, # type: ignore[arg-type] read_paths_method=read_paths_method, included_paths=included_paths, ignored_paths=ignored_paths, diff --git a/src/OpenApiLibCore/__init__.py b/src/OpenApiLibCore/__init__.py index b4d373e..27d4e82 100644 --- a/src/OpenApiLibCore/__init__.py +++ b/src/OpenApiLibCore/__init__.py @@ -7,14 +7,17 @@ - IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint, UniquePropertyValueConstraint: Classes to be subclassed by the library user when implementing a custom mapping module (advanced use). -- Dto, Relation: Base classes that can be used for type annotations. +- RelationsMapping, Relation: Base classes that can be used for type annotations. - IGNORE: A special constant that can be used as a value in the PropertyValueConstraint. """ from importlib.metadata import version -from OpenApiLibCore.dto_base import ( - Dto, +from OpenApiLibCore.data_relations.relations_base import RelationsMapping +from OpenApiLibCore.keyword_logic.validation import ValidationLevel +from OpenApiLibCore.models import IGNORE, UNSET +from OpenApiLibCore.models.request_data import RequestData, RequestValues +from OpenApiLibCore.models.resource_relations import ( IdDependency, IdReference, PathPropertiesConstraint, @@ -22,13 +25,7 @@ ResourceRelation, UniquePropertyValueConstraint, ) -from OpenApiLibCore.dto_utils import DefaultDto -from OpenApiLibCore.openapi_libcore import ( - OpenApiLibCore, -) -from OpenApiLibCore.request_data import RequestData, RequestValues -from OpenApiLibCore.validation import ValidationLevel -from OpenApiLibCore.value_utils import IGNORE, UNSET +from OpenApiLibCore.openapi_libcore import OpenApiLibCore try: __version__ = version("robotframework-openapi-libcore") @@ -43,6 +40,8 @@ "set_auth", "set_extra_headers", "get_request_values", + "get_request_values_object", + "convert_request_values_to_dict", "get_request_data", "get_invalid_body_data", "get_invalidated_parameters", @@ -54,6 +53,8 @@ "get_invalidated_url", "ensure_in_use", "authorized_request", + "perform_authorized_request", + "validated_request", "perform_validated_request", "validate_response_using_validator", "assert_href_to_resource_is_valid", @@ -65,13 +66,12 @@ __all__ = [ "IGNORE", "UNSET", - "DefaultDto", - "Dto", "IdDependency", "IdReference", "OpenApiLibCore", "PathPropertiesConstraint", "PropertyValueConstraint", + "RelationsMapping", "RequestData", "RequestValues", "ResourceRelation", diff --git a/src/OpenApiLibCore/annotations.py b/src/OpenApiLibCore/annotations.py index fa70867..2cb6a5e 100644 --- a/src/OpenApiLibCore/annotations.py +++ b/src/OpenApiLibCore/annotations.py @@ -6,5 +6,5 @@ JSON = TypeAliasType( "JSON", - "Union[dict[str, JSON], list[JSON], str, bytes, int, float, bool, None]", + "Union[dict[str, JSON], list[JSON], str, int, float, bool, None]", ) diff --git a/src/OpenApiLibCore/data_generation/__init__.py b/src/OpenApiLibCore/data_generation/__init__.py index be5f703..c41511c 100644 --- a/src/OpenApiLibCore/data_generation/__init__.py +++ b/src/OpenApiLibCore/data_generation/__init__.py @@ -2,9 +2,3 @@ Module holding the functions related to data generation for the requests made as part of keyword exection. """ - -from .data_generation_core import get_request_data - -__all__ = [ - "get_request_data", -] diff --git a/src/OpenApiLibCore/data_generation/body_data_generation.py b/src/OpenApiLibCore/data_generation/body_data_generation.py deleted file mode 100644 index 0ae0270..0000000 --- a/src/OpenApiLibCore/data_generation/body_data_generation.py +++ /dev/null @@ -1,250 +0,0 @@ -""" -Module holding the functions related to (json) data generation -for the body of requests made as part of keyword exection. -""" - -from random import choice, randint, sample -from typing import Any - -from robot.api import logger - -import OpenApiLibCore.path_functions as _path_functions -from OpenApiLibCore.annotations import JSON -from OpenApiLibCore.dto_base import ( - Dto, - IdDependency, - PropertyValueConstraint, -) -from OpenApiLibCore.dto_utils import DefaultDto -from OpenApiLibCore.models import ( - ArraySchema, - ObjectSchema, - SchemaObjectTypes, - UnionTypeSchema, -) -from OpenApiLibCore.parameter_utils import get_safe_name_for_oas_name -from OpenApiLibCore.protocols import GetIdPropertyNameType -from OpenApiLibCore.value_utils import IGNORE - - -def get_json_data_for_dto_class( - schema: SchemaObjectTypes, - dto_class: type[Dto], - get_id_property_name: GetIdPropertyNameType, - operation_id: str | None = None, -) -> JSON: - if isinstance(schema, UnionTypeSchema): - chosen_schema = choice(schema.resolved_schemas) - return get_json_data_for_dto_class( - schema=chosen_schema, - dto_class=dto_class, - get_id_property_name=get_id_property_name, - operation_id=operation_id, - ) - - match schema: - case ObjectSchema(): - return get_dict_data_for_dto_class( - schema=schema, - dto_class=dto_class, - get_id_property_name=get_id_property_name, - operation_id=operation_id, - ) - case ArraySchema(): - return get_list_data_for_dto_class( - schema=schema, - dto_class=dto_class, - get_id_property_name=get_id_property_name, - operation_id=operation_id, - ) - case _: - return schema.get_valid_value() - - -def get_dict_data_for_dto_class( - schema: ObjectSchema, - dto_class: type[Dto], - get_id_property_name: GetIdPropertyNameType, - operation_id: str | None = None, -) -> dict[str, Any]: - json_data: dict[str, Any] = {} - - property_names = get_property_names_to_process(schema=schema, dto_class=dto_class) - - for property_name in property_names: - property_schema = schema.properties.root[property_name] # type: ignore[union-attr] - if property_schema.readOnly: - continue - - json_data[property_name] = get_data_for_property( - property_name=property_name, - property_schema=property_schema, - get_id_property_name=get_id_property_name, - dto_class=dto_class, - operation_id=operation_id, - ) - - return json_data - - -def get_list_data_for_dto_class( - schema: ArraySchema, - dto_class: type[Dto], - get_id_property_name: GetIdPropertyNameType, - operation_id: str | None = None, -) -> list[JSON]: - json_data: list[JSON] = [] - list_item_schema = schema.items - min_items = schema.minItems if schema.minItems is not None else 0 - max_items = schema.maxItems if schema.maxItems is not None else 1 - number_of_items_to_generate = randint(min_items, max_items) - for _ in range(number_of_items_to_generate): - list_item_data = get_json_data_for_dto_class( - schema=list_item_schema, - dto_class=dto_class, - get_id_property_name=get_id_property_name, - operation_id=operation_id, - ) - json_data.append(list_item_data) - return json_data - - -def get_data_for_property( - property_name: str, - property_schema: SchemaObjectTypes, - get_id_property_name: GetIdPropertyNameType, - dto_class: type[Dto], - operation_id: str | None, -) -> JSON: - if constrained_values := get_constrained_values( - dto_class=dto_class, property_name=property_name - ): - constrained_value = choice(constrained_values) - # Check if the chosen value is a nested Dto; since a Dto is never - # instantiated, we can use isinstance(..., type) for this. - if isinstance(constrained_value, type): - return get_value_constrained_by_nested_dto( - property_schema=property_schema, - nested_dto_class=constrained_value, - get_id_property_name=get_id_property_name, - operation_id=operation_id, - ) - return constrained_value - - if ( - dependent_id := get_dependent_id( - dto_class=dto_class, - property_name=property_name, - operation_id=operation_id, - get_id_property_name=get_id_property_name, - ) - ) is not None: - return dependent_id - - return get_json_data_for_dto_class( - schema=property_schema, - dto_class=DefaultDto, - get_id_property_name=get_id_property_name, - ) - - -def get_value_constrained_by_nested_dto( - property_schema: SchemaObjectTypes, - nested_dto_class: type[Dto], - get_id_property_name: GetIdPropertyNameType, - operation_id: str | None, -) -> JSON: - nested_schema = get_schema_for_nested_dto(property_schema=property_schema) - nested_value = get_json_data_for_dto_class( - schema=nested_schema, - dto_class=nested_dto_class, - get_id_property_name=get_id_property_name, - operation_id=operation_id, - ) - return nested_value - - -def get_schema_for_nested_dto(property_schema: SchemaObjectTypes) -> SchemaObjectTypes: - if isinstance(property_schema, UnionTypeSchema): - chosen_schema = choice(property_schema.resolved_schemas) - return get_schema_for_nested_dto(chosen_schema) - - return property_schema - - -def get_property_names_to_process( - schema: ObjectSchema, - dto_class: type[Dto], -) -> list[str]: - property_names = [] - - for property_name in schema.properties.root: # type: ignore[union-attr] - # register the oas_name - _ = get_safe_name_for_oas_name(property_name) - if constrained_values := get_constrained_values( - dto_class=dto_class, property_name=property_name - ): - # do not add properties that are configured to be ignored - if IGNORE in constrained_values: # type: ignore[comparison-overlap] - continue - property_names.append(property_name) - - max_properties = schema.maxProperties - if max_properties and len(property_names) > max_properties: - required_properties = schema.required - number_of_optional_properties = max_properties - len(required_properties) - optional_properties = [ - name for name in property_names if name not in required_properties - ] - selected_optional_properties = sample( - optional_properties, number_of_optional_properties - ) - property_names = required_properties + selected_optional_properties - - return property_names - - -def get_constrained_values( - dto_class: type[Dto], property_name: str -) -> list[JSON | type[Dto]]: - relations = dto_class.get_relations() - values_list = [ - c.values - for c in relations - if (isinstance(c, PropertyValueConstraint) and c.property_name == property_name) - ] - # values should be empty or contain 1 list of allowed values - return values_list.pop() if values_list else [] - - -def get_dependent_id( - dto_class: type[Dto], - property_name: str, - operation_id: str | None, - get_id_property_name: GetIdPropertyNameType, -) -> str | int | float | None: - relations = dto_class.get_relations() - # multiple get paths are possible based on the operation being performed - id_get_paths = [ - (d.get_path, d.operation_id) - for d in relations - if (isinstance(d, IdDependency) and d.property_name == property_name) - ] - if not id_get_paths: - return None - if len(id_get_paths) == 1: - id_get_path, _ = id_get_paths.pop() - else: - try: - [id_get_path] = [ - path for path, operation in id_get_paths if operation == operation_id - ] - # There could be multiple get_paths, but not one for the current operation - except ValueError: - return None - - valid_id = _path_functions.get_valid_id_for_path( - path=id_get_path, get_id_property_name=get_id_property_name - ) - logger.debug(f"get_dependent_id for {id_get_path} returned {valid_id}") - return valid_id diff --git a/src/OpenApiLibCore/data_generation/data_generation_core.py b/src/OpenApiLibCore/data_generation/data_generation_core.py index 71f3ac6..a041cbb 100644 --- a/src/OpenApiLibCore/data_generation/data_generation_core.py +++ b/src/OpenApiLibCore/data_generation/data_generation_core.py @@ -10,68 +10,65 @@ from robot.api import logger -import OpenApiLibCore.path_functions as _path_functions +import OpenApiLibCore.keyword_logic.path_functions as _path_functions from OpenApiLibCore.annotations import JSON -from OpenApiLibCore.dto_base import ( - Dto, - PropertyValueConstraint, - ResourceRelation, -) -from OpenApiLibCore.dto_utils import DefaultDto -from OpenApiLibCore.models import ( +from OpenApiLibCore.data_relations.relations_base import RelationsMapping +from OpenApiLibCore.models import IGNORE +from OpenApiLibCore.models.oas_models import ( + ArraySchema, ObjectSchema, OpenApiObject, OperationObject, ParameterObject, + ResolvedSchemaObjectTypes, UnionTypeSchema, ) -from OpenApiLibCore.parameter_utils import get_safe_name_for_oas_name -from OpenApiLibCore.protocols import GetDtoClassType, GetIdPropertyNameType -from OpenApiLibCore.request_data import RequestData -from OpenApiLibCore.value_utils import IGNORE - -from .body_data_generation import ( - get_json_data_for_dto_class as _get_json_data_for_dto_class, +from OpenApiLibCore.models.request_data import RequestData +from OpenApiLibCore.models.resource_relations import ( + PropertyValueConstraint, + ResourceRelation, ) +from OpenApiLibCore.protocols import RelationsMappingType +from OpenApiLibCore.utils.parameter_utils import get_safe_name_for_oas_name def get_request_data( path: str, method: str, - get_dto_class: GetDtoClassType, - get_id_property_name: GetIdPropertyNameType, openapi_spec: OpenApiObject, ) -> RequestData: method = method.lower() - dto_cls_name = get_dto_cls_name(path=path, method=method) + mapping_cls_name = get_mapping_cls_name(path=path, method=method) # The path can contain already resolved Ids that have to be matched # against the parametrized paths in the paths section. spec_path = _path_functions.get_parametrized_path( path=path, openapi_spec=openapi_spec ) - dto_class = get_dto_class(path=spec_path, method=method) try: path_item = openapi_spec.paths[spec_path] operation_spec: OperationObject | None = getattr(path_item, method) if operation_spec is None: raise AttributeError + relations_mapping = operation_spec.relations_mapping except AttributeError: logger.info( f"method '{method}' not supported on '{spec_path}, using empty spec." ) operation_spec = OperationObject(operationId="") + relations_mapping = None parameters, params, headers = get_request_parameters( - dto_class=dto_class, method_spec=operation_spec + relations_mapping=relations_mapping, method_spec=operation_spec ) if operation_spec.requestBody is None: - dto_instance = _get_dto_instance_for_empty_body( - dto_class=dto_class, - dto_cls_name=dto_cls_name, + relations_mapping = _get_mapping_dataclass_for_empty_body( + relations_mapping=relations_mapping, + mapping_cls_name=mapping_cls_name, method_spec=operation_spec, ) return RequestData( - dto=dto_instance, + valid_data=None, + relations_mapping=relations_mapping, parameters=parameters, params=params, headers=headers, @@ -85,92 +82,113 @@ def get_request_data( f"No supported content schema found: {operation_spec.requestBody.content}" ) - headers.update({"content-type": operation_spec.requestBody.mime_type}) - - if isinstance(body_schema, UnionTypeSchema): - resolved_schemas = body_schema.resolved_schemas - body_schema = choice(resolved_schemas) - - if not isinstance(body_schema, ObjectSchema): - raise ValueError(f"Selected schema is not an object schema: {body_schema}") + if operation_spec.requestBody.mime_type: # pragma: no branch + if "content-type" in headers: # pragma: no cover + key_value = "content-type" + else: + key_value = "Content-Type" + headers.update({key_value: operation_spec.requestBody.mime_type}) - dto_data = _get_json_data_for_dto_class( - schema=body_schema, - dto_class=dto_class, - get_id_property_name=get_id_property_name, - operation_id=operation_spec.operationId, + valid_data, schema_used_for_data_generation = body_schema.get_valid_value( + operation_id=operation_spec.operationId ) - dto_instance = _get_dto_instance_from_dto_data( - object_schema=body_schema, - dto_class=dto_class, - dto_data=dto_data, + + relations_mapping = _get_mapping_dataclass_from_valid_data( + schema=schema_used_for_data_generation, + relations_mapping=relations_mapping, + valid_data=valid_data, method_spec=operation_spec, - dto_cls_name=dto_cls_name, + mapping_cls_name=mapping_cls_name, ) return RequestData( - dto=dto_instance, - body_schema=body_schema, + valid_data=valid_data, + relations_mapping=relations_mapping, + body_schema=schema_used_for_data_generation, parameters=parameters, params=params, headers=headers, ) -def _get_dto_instance_for_empty_body( - dto_class: type[Dto], - dto_cls_name: str, +def _get_mapping_dataclass_for_empty_body( + relations_mapping: RelationsMappingType | None, + mapping_cls_name: str, method_spec: OperationObject, -) -> Dto: - if dto_class == DefaultDto: - dto_instance: Dto = DefaultDto() - else: - cls_name = method_spec.operationId if method_spec.operationId else dto_cls_name - dto_class = make_dataclass( - cls_name=cls_name, - fields=[], - bases=(dto_class,), - ) - dto_instance = dto_class() - return dto_instance +) -> RelationsMappingType: + cls_name = method_spec.operationId if method_spec.operationId else mapping_cls_name + base = relations_mapping if relations_mapping else RelationsMapping + mapping_class = make_dataclass( + cls_name=cls_name, + fields=[], + bases=(base,), + ) + return mapping_class -def _get_dto_instance_from_dto_data( - object_schema: ObjectSchema, - dto_class: type[Dto], - dto_data: JSON, +def _get_mapping_dataclass_from_valid_data( + schema: ResolvedSchemaObjectTypes, + relations_mapping: RelationsMappingType | None, + valid_data: JSON, method_spec: OperationObject, - dto_cls_name: str, -) -> Dto: - if not isinstance(dto_data, (dict, list)): - return DefaultDto() + mapping_cls_name: str, +) -> RelationsMappingType: + if not isinstance(schema, (ObjectSchema, ArraySchema)): + return _get_mapping_dataclass_for_empty_body( + relations_mapping=relations_mapping, + mapping_cls_name=mapping_cls_name, + method_spec=method_spec, + ) - if isinstance(dto_data, list): - raise NotImplementedError + if isinstance(schema, ArraySchema): + if not valid_data or not isinstance(valid_data, list): + return _get_mapping_dataclass_for_empty_body( + relations_mapping=relations_mapping, + mapping_cls_name=mapping_cls_name, + method_spec=method_spec, + ) + first_item_data = valid_data[0] + item_object_schema = schema.items + + if isinstance(item_object_schema, UnionTypeSchema): + resolved_schemas = item_object_schema.resolved_schemas + for resolved_schema in resolved_schemas: + matched_schema = resolved_schema + if isinstance(first_item_data, resolved_schema.python_type): + break + else: + matched_schema = item_object_schema + + mapping_dataclass = _get_mapping_dataclass_from_valid_data( + schema=matched_schema, + relations_mapping=relations_mapping, + valid_data=first_item_data, + method_spec=method_spec, + mapping_cls_name=mapping_cls_name, + ) + return mapping_dataclass - fields = get_fields_from_dto_data(object_schema, dto_data) - cls_name = method_spec.operationId if method_spec.operationId else dto_cls_name - dto_class_ = make_dataclass( + assert isinstance(valid_data, dict), ( + "Data consistency error: schema is of type ObjectSchema but valid_data is not a dict." + ) + fields = get_dataclass_fields(object_schema=schema, valid_data=valid_data) + cls_name = method_spec.operationId if method_spec.operationId else mapping_cls_name + base = relations_mapping if relations_mapping else RelationsMapping + mapping_dataclass = make_dataclass( cls_name=cls_name, fields=fields, - bases=(dto_class,), + bases=(base,), ) - # dto_data = {get_safe_key(key): value for key, value in dto_data.items()} - dto_data = { - get_safe_name_for_oas_name(key): value for key, value in dto_data.items() - } - return cast(Dto, dto_class_(**dto_data)) + return mapping_dataclass -def get_fields_from_dto_data( - object_schema: ObjectSchema, dto_data: dict[str, JSON] +def get_dataclass_fields( + object_schema: ObjectSchema, valid_data: dict[str, JSON] ) -> list[tuple[str, type[object], Field[object]]]: - """Get a dataclasses fields list based on the content_schema and dto_data.""" + """Get a dataclasses fields list based on the object_schema and valid_data.""" fields: list[tuple[str, type[object], Field[object]]] = [] - for key, value in dto_data.items(): - # safe_key = get_safe_key(key) + for key, value in valid_data.items(): safe_key = get_safe_name_for_oas_name(key) - # metadata = {"original_property_name": key} if key in object_schema.required: # The fields list is used to create a dataclass, so non-default fields # must go before fields with a default @@ -182,7 +200,7 @@ def get_fields_from_dto_data( return fields -def get_dto_cls_name(path: str, method: str) -> str: +def get_mapping_cls_name(path: str, method: str) -> str: method = method.capitalize() path = path.translate({ord(i): None for i in "{}"}) path_parts = path.split("/") @@ -192,11 +210,13 @@ def get_dto_cls_name(path: str, method: str) -> str: def get_request_parameters( - dto_class: Dto | type[Dto], method_spec: OperationObject + relations_mapping: RelationsMappingType | None, method_spec: OperationObject ) -> tuple[list[ParameterObject], dict[str, Any], dict[str, str]]: """Get the methods parameter spec and params and headers with valid data.""" parameters = method_spec.parameters if method_spec.parameters else [] - parameter_relations = dto_class.get_parameter_relations() + parameter_relations = ( + relations_mapping.get_parameter_relations() if relations_mapping else [] + ) query_params = [p for p in parameters if p.in_ == "query"] header_params = [p for p in parameters if p.in_ == "header"] params = get_parameter_data(query_params, parameter_relations) @@ -229,7 +249,7 @@ def get_parameter_data( continue if parameter.schema_ is None: - continue - value = parameter.schema_.get_valid_value() + continue # pragma: no cover + value = parameter.schema_.get_valid_value()[0] result[parameter_name] = value return result diff --git a/src/OpenApiLibCore/data_invalidation.py b/src/OpenApiLibCore/data_generation/data_invalidation.py similarity index 61% rename from src/OpenApiLibCore/data_invalidation.py rename to src/OpenApiLibCore/data_generation/data_invalidation.py index 63fa745..e46107e 100644 --- a/src/OpenApiLibCore/data_invalidation.py +++ b/src/OpenApiLibCore/data_generation/data_invalidation.py @@ -5,80 +5,149 @@ from copy import deepcopy from random import choice -from typing import Any +from typing import Any, Literal, overload from requests import Response from robot.api import logger from robot.libraries.BuiltIn import BuiltIn from OpenApiLibCore.annotations import JSON -from OpenApiLibCore.dto_base import ( +from OpenApiLibCore.data_relations.relations_base import RelationsMapping +from OpenApiLibCore.models import IGNORE +from OpenApiLibCore.models.oas_models import ( + ArraySchema, + ObjectSchema, + ParameterObject, + UnionTypeSchema, +) +from OpenApiLibCore.models.request_data import RequestData +from OpenApiLibCore.models.resource_relations import ( NOT_SET, - Dto, IdReference, PropertyValueConstraint, UniquePropertyValueConstraint, ) -from OpenApiLibCore.models import ParameterObject, UnionTypeSchema -from OpenApiLibCore.request_data import RequestData -from OpenApiLibCore.value_utils import IGNORE, get_invalid_value run_keyword = BuiltIn().run_keyword +@overload +def _run_keyword( + keyword_name: Literal["get_json_data_with_conflict"], *args: object +) -> dict[str, JSON]: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["ensure_in_use"], *args: object +) -> None: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_request_data"], *args: str +) -> RequestData: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["authorized_request"], *args: object +) -> Response: ... # pragma: no cover + + +def _run_keyword(keyword_name: str, *args: object) -> object: + return run_keyword(keyword_name, *args) # pyright: ignore[reportArgumentType] + + def get_invalid_body_data( url: str, method: str, status_code: int, request_data: RequestData, - invalid_property_default_response: int, -) -> dict[str, Any]: + invalid_data_default_response: int, +) -> JSON: method = method.lower() - data_relations = request_data.dto.get_body_relations_for_error_code(status_code) + data_relations = request_data.relations_mapping.get_body_relations_for_error_code( + status_code + ) if not data_relations: if request_data.body_schema is None: raise ValueError( "Failed to invalidate: request_data does not contain a body_schema." ) - json_data = request_data.dto.get_invalidated_data( - schema=request_data.body_schema, + + if not isinstance(request_data.body_schema, (ArraySchema, ObjectSchema)): + raise NotImplementedError("primitive types not supported for body data.") + + if isinstance(request_data.body_schema, ArraySchema): + if not isinstance(request_data.valid_data, list): + raise ValueError("Type of valid_data does not match body_schema type.") + invalid_item_data: list[JSON] = request_data.body_schema.get_invalid_data( + valid_data=request_data.valid_data, + status_code=status_code, + invalid_property_default_code=invalid_data_default_response, + ) + return [invalid_item_data] + + if not isinstance(request_data.valid_data, dict): + raise ValueError("Type of valid_data does not match body_schema type.") + json_data = request_data.body_schema.get_invalid_data( + valid_data=request_data.valid_data, status_code=status_code, - invalid_property_default_code=invalid_property_default_response, + invalid_property_default_code=invalid_data_default_response, ) return json_data + resource_relation = choice(data_relations) if isinstance(resource_relation, UniquePropertyValueConstraint): - json_data = run_keyword( + return _run_keyword( "get_json_data_with_conflict", url, method, - request_data.dto, + request_data.valid_data, + request_data.relations_mapping, status_code, ) - elif isinstance(resource_relation, IdReference): - run_keyword("ensure_in_use", url, resource_relation) - json_data = request_data.dto.as_dict() - else: - if request_data.body_schema is None: - raise ValueError( - "Failed to invalidate: request_data does not contain a body_schema." - ) - json_data = request_data.dto.get_invalidated_data( - schema=request_data.body_schema, + if isinstance(resource_relation, IdReference): + _run_keyword("ensure_in_use", url, resource_relation) + return request_data.valid_data + + if request_data.body_schema is None: + raise ValueError( + "Failed to invalidate: request_data does not contain a body_schema." + ) + if not isinstance(request_data.body_schema, (ArraySchema, ObjectSchema)): + raise NotImplementedError("primitive types not supported for body data.") + + if isinstance(request_data.body_schema, ArraySchema): + if not isinstance(request_data.valid_data, list): + raise ValueError("Type of valid_data does not match body_schema type.") + invalid_item_data = request_data.body_schema.get_invalid_data( + valid_data=request_data.valid_data, status_code=status_code, - invalid_property_default_code=invalid_property_default_response, + invalid_property_default_code=invalid_data_default_response, ) - return json_data + return [invalid_item_data] + + if not isinstance(request_data.valid_data, dict): + raise ValueError("Type of valid_data does not match body_schema type.") + return request_data.body_schema.get_invalid_data( + valid_data=request_data.valid_data, + status_code=status_code, + invalid_property_default_code=invalid_data_default_response, + ) def get_invalidated_parameters( - status_code: int, request_data: RequestData, invalid_property_default_response: int -) -> tuple[dict[str, JSON], dict[str, JSON]]: + status_code: int, request_data: RequestData, invalid_data_default_response: int +) -> tuple[dict[str, JSON], dict[str, str]]: if not request_data.parameters: raise ValueError("No params or headers to invalidate.") # ensure the status_code can be triggered - relations = request_data.dto.get_parameter_relations_for_error_code(status_code) + relations = request_data.relations_mapping.get_parameter_relations_for_error_code( + status_code + ) relations_for_status_code = [ r for r in relations @@ -92,14 +161,14 @@ def get_invalidated_parameters( } relation_property_names = {r.property_name for r in relations_for_status_code} if not relation_property_names: - if status_code != invalid_property_default_response: + if status_code != invalid_data_default_response: raise ValueError(f"No relations to cause status_code {status_code} found.") # ensure we're not modifying mutable properties params = deepcopy(request_data.params) headers = deepcopy(request_data.headers) - if status_code == invalid_property_default_response: + if status_code == invalid_data_default_response: # take the params and headers that can be invalidated based on data type # and expand the set with properties that can be invalided by relations parameter_names = set(request_data.params_that_can_be_invalidated).union( @@ -114,8 +183,8 @@ def get_invalidated_parameters( # non-default status_codes can only be the result of a Relation parameter_names = relation_property_names - # Dto mappings may contain generic mappings for properties that are not present - # in this specific schema + # Relation mappings may contain generic mappings for properties that are + # not present in this specific schema request_data_parameter_names = [p.name for p in request_data.parameters] additional_relation_property_names = { n for n in relation_property_names if n not in request_data_parameter_names @@ -164,7 +233,7 @@ def get_invalidated_parameters( except ValueError: invalid_value_for_error_code = NOT_SET - # get the constraint values if available for the chosen parameter + # get the constrained values if available for the chosen parameter try: [values_from_constraint] = [ r.values @@ -197,19 +266,17 @@ def get_invalidated_parameters( raise ValueError(f"No schema defined for parameter: {parameter_data}.") if isinstance(value_schema, UnionTypeSchema): - # FIXME: extra handling may be needed in case of values_from_constraint value_schema = choice(value_schema.resolved_schemas) - invalid_value = get_invalid_value( - value_schema=value_schema, - current_value=valid_value, + invalid_value = value_schema.get_invalid_value( + valid_value=valid_value, # type: ignore[arg-type] values_from_constraint=values_from_constraint, ) logger.debug(f"{parameter_to_invalidate} changed to {invalid_value}") # update the params / headers and return if parameter_to_invalidate in params.keys(): - params[parameter_to_invalidate] = invalid_value + params[parameter_to_invalidate] = invalid_value # pyright: ignore[reportArgumentType] else: headers[parameter_to_invalidate] = str(invalid_value) return params, headers @@ -218,10 +285,10 @@ def get_invalidated_parameters( def ensure_parameter_in_parameters( parameter_to_invalidate: str, params: dict[str, JSON], - headers: dict[str, JSON], + headers: dict[str, str], parameter_data: ParameterObject, values_from_constraint: list[JSON], -) -> tuple[dict[str, JSON], dict[str, JSON]]: +) -> tuple[dict[str, JSON], dict[str, str]]: """ Returns the params, headers tuple with parameter_to_invalidate with a valid value to params or headers if not originally present. @@ -239,7 +306,7 @@ def ensure_parameter_in_parameters( if isinstance(value_schema, UnionTypeSchema): value_schema = choice(value_schema.resolved_schemas) - valid_value = value_schema.get_valid_value() + valid_value = value_schema.get_valid_value()[0] if ( parameter_data.in_ == "query" and parameter_to_invalidate not in params.keys() @@ -254,12 +321,18 @@ def ensure_parameter_in_parameters( def get_json_data_with_conflict( - url: str, base_url: str, method: str, dto: Dto, conflict_status_code: int + url: str, + base_url: str, + method: str, + json_data: dict[str, JSON], + relations_mapping: type[RelationsMapping], + conflict_status_code: int, ) -> dict[str, Any]: method = method.lower() - json_data = dto.as_dict() unique_property_value_constraints = [ - r for r in dto.get_relations() if isinstance(r, UniquePropertyValueConstraint) + r + for r in relations_mapping.get_relations() + if isinstance(r, UniquePropertyValueConstraint) ] for relation in unique_property_value_constraints: json_data[relation.property_name] = relation.value @@ -267,21 +340,22 @@ def get_json_data_with_conflict( if method in ["patch", "put"]: post_url_parts = url.split("/")[:-1] post_url = "/".join(post_url_parts) - # the PATCH or PUT may use a different dto than required for POST - # so a valid POST dto must be constructed + # the PATCH or PUT may use a different relations_mapping than required for + # POST so valid POST data must be constructed path = post_url.replace(base_url, "") - request_data: RequestData = run_keyword("get_request_data", path, "post") - post_json = request_data.dto.as_dict() - for key in post_json.keys(): - if key in json_data: - post_json[key] = json_data.get(key) + request_data = _run_keyword("get_request_data", path, "post") + post_json = request_data.valid_data + if isinstance(post_json, dict): + for key in post_json.keys(): + if key in json_data: + post_json[key] = json_data.get(key) else: post_url = url post_json = json_data path = post_url.replace(base_url, "") - request_data = run_keyword("get_request_data", path, "post") + request_data = _run_keyword("get_request_data", path, "post") - response: Response = run_keyword( + response = _run_keyword( "authorized_request", post_url, "post", @@ -295,5 +369,6 @@ def get_json_data_with_conflict( ) return json_data raise ValueError( - f"No UniquePropertyValueConstraint in the get_relations list on dto {dto}." + f"No UniquePropertyValueConstraint in the get_relations list on " + f"relations_mapping {relations_mapping}." ) diff --git a/src/OpenApiLibCore/localized_faker.py b/src/OpenApiLibCore/data_generation/localized_faker.py similarity index 100% rename from src/OpenApiLibCore/localized_faker.py rename to src/OpenApiLibCore/data_generation/localized_faker.py diff --git a/src/OpenApiLibCore/data_generation/value_utils.py b/src/OpenApiLibCore/data_generation/value_utils.py new file mode 100644 index 0000000..81dc28c --- /dev/null +++ b/src/OpenApiLibCore/data_generation/value_utils.py @@ -0,0 +1,39 @@ +"""Utility module with functions to handle OpenAPI value types and restrictions.""" + + +def json_type_name_of_python_type(python_type: type) -> str: + """Return the JSON type name for supported Python types.""" + if python_type == str: + return "string" + if python_type == bool: + return "boolean" + if python_type == int: + return "integer" + if python_type == float: + return "number" + if python_type == list: + return "array" + if python_type == dict: + return "object" + if python_type == type(None): + return "null" + raise ValueError(f"No json type mapping for Python type {python_type} available.") + + +def python_type_by_json_type_name(type_name: str) -> type: + """Return the Python type based on the JSON type name.""" + if type_name == "string": + return str + if type_name == "boolean": + return bool + if type_name == "integer": + return int + if type_name == "number": + return float + if type_name == "array": + return list + if type_name == "object": + return dict + if type_name == "null": + return type(None) + raise ValueError(f"No Python type mapping for JSON type '{type_name}' available.") diff --git a/src/OpenApiLibCore/data_relations/__init__.py b/src/OpenApiLibCore/data_relations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/OpenApiLibCore/data_relations/relations_base.py b/src/OpenApiLibCore/data_relations/relations_base.py new file mode 100644 index 0000000..3a4c071 --- /dev/null +++ b/src/OpenApiLibCore/data_relations/relations_base.py @@ -0,0 +1,142 @@ +""" +Module holding the (base) classes that can be used by the user of the OpenApiLibCore +to implement custom mappings for dependencies between resources in the API under +test and constraints / restrictions on properties of the resources. +""" + +from abc import ABC +from dataclasses import dataclass +from importlib import import_module +from typing import Callable + +from robot.api import logger + +from OpenApiLibCore.models.resource_relations import ( + NOT_SET, + PathPropertiesConstraint, + ResourceRelation, +) +from OpenApiLibCore.protocols import ( + IGetIdPropertyName, + RelationsMappingType, +) +from OpenApiLibCore.utils.id_mapping import dummy_transformer + + +@dataclass +class RelationsMapping(ABC): + """Base class for the RelationsMapping classes.""" + + @staticmethod + def get_path_relations() -> list[PathPropertiesConstraint]: + """Return the list of path-related Relations.""" + return [] + + @staticmethod + def get_parameter_relations() -> list[ResourceRelation]: + """Return the list of Relations for the header and query parameters.""" + return [] + + @classmethod + def get_parameter_relations_for_error_code( + cls, error_code: int + ) -> list[ResourceRelation]: + """Return the list of Relations associated with the given error_code.""" + relations: list[ResourceRelation] = [ + r + for r in cls.get_parameter_relations() + if r.error_code == error_code + or ( + getattr(r, "invalid_value_error_code", None) == error_code + and getattr(r, "invalid_value", None) != NOT_SET + ) + ] + return relations + + @staticmethod + def get_relations() -> list[ResourceRelation]: + """Return the list of Relations for the (json) body.""" + return [] + + @classmethod + def get_body_relations_for_error_code( + cls, error_code: int + ) -> list[ResourceRelation]: + """ + Return the list of Relations associated with the given error_code that are + applicable to the body / payload of the request. + """ + relations: list[ResourceRelation] = [ + r + for r in cls.get_relations() + if r.error_code == error_code + or ( + getattr(r, "invalid_value_error_code", None) == error_code + and getattr(r, "invalid_value", None) != NOT_SET + ) + ] + return relations + + +def get_relations_mapping_dict( + mappings_module_name: str, +) -> dict[tuple[str, str], RelationsMappingType]: + try: + mappings_module = import_module(mappings_module_name) + return mappings_module.RELATIONS_MAPPING # type: ignore[no-any-return] + except (ImportError, AttributeError, ValueError) as exception: + if mappings_module_name != "no mapping": + logger.error(f"RELATIONS_MAPPING was not imported: {exception}") + return {} + + +def get_path_mapping_dict( + mappings_module_name: str, +) -> dict[str, RelationsMappingType]: + try: + mappings_module = import_module(mappings_module_name) + return mappings_module.PATH_MAPPING # type: ignore[no-any-return] + except (ImportError, AttributeError, ValueError) as exception: + if mappings_module_name != "no mapping": + logger.error(f"PATH_MAPPING was not imported: {exception}") + return {} + + +def get_id_property_name( + mappings_module_name: str, default_id_property_name: str +) -> IGetIdPropertyName: + return GetIdPropertyName( + mappings_module_name=mappings_module_name, + default_id_property_name=default_id_property_name, + ) + + +class GetIdPropertyName: + """ + Callable class to return the name of the property that uniquely identifies + the resource from user-implemented mappings file. + """ + + def __init__( + self, mappings_module_name: str, default_id_property_name: str + ) -> None: + self.default_id_property_name = default_id_property_name + try: + mappings_module = import_module(mappings_module_name) + self.id_mapping: dict[ + str, + str | tuple[str, Callable[[str], str]], + ] = mappings_module.ID_MAPPING + except (ImportError, AttributeError, ValueError) as exception: + if mappings_module_name != "no mapping": + logger.error(f"ID_MAPPING was not imported: {exception}") + self.id_mapping = {} + + def __call__(self, path: str) -> tuple[str, Callable[[str], str]]: + try: + value_or_mapping = self.id_mapping[path] + if isinstance(value_or_mapping, str): + return (value_or_mapping, dummy_transformer) + return value_or_mapping + except KeyError: + return (self.default_id_property_name, dummy_transformer) diff --git a/src/OpenApiLibCore/dto_base.py b/src/OpenApiLibCore/dto_base.py deleted file mode 100644 index b646103..0000000 --- a/src/OpenApiLibCore/dto_base.py +++ /dev/null @@ -1,260 +0,0 @@ -""" -Module holding the (base) classes that can be used by the user of the OpenApiLibCore -to implement custom mappings for dependencies between resources in the API under -test and constraints / restrictions on properties of the resources. -""" - -from abc import ABC -from dataclasses import dataclass, fields -from random import choice, shuffle -from typing import Any -from uuid import uuid4 - -from robot.api import logger - -from OpenApiLibCore import value_utils -from OpenApiLibCore.models import NullSchema, ObjectSchema, UnionTypeSchema -from OpenApiLibCore.parameter_utils import get_oas_name_from_safe_name - -NOT_SET = object() -SENTINEL = object() - - -class ResourceRelation(ABC): - """ABC for all resource relations or restrictions within the API.""" - - property_name: str - error_code: int - - -@dataclass -class PathPropertiesConstraint(ResourceRelation): - """The value to be used as the ``path`` for related requests.""" - - path: str - property_name: str = "id" - invalid_value: Any = NOT_SET - invalid_value_error_code: int = 422 - error_code: int = 404 - - -@dataclass -class PropertyValueConstraint(ResourceRelation): - """The allowed values for property_name.""" - - property_name: str - values: list[Any] - invalid_value: Any = NOT_SET - invalid_value_error_code: int = 422 - error_code: int = 422 - treat_as_mandatory: bool = False - - -@dataclass -class IdDependency(ResourceRelation): - """The path where a valid id for the property_name can be gotten (using GET).""" - - property_name: str - get_path: str - operation_id: str = "" - error_code: int = 422 - - -@dataclass -class IdReference(ResourceRelation): - """The path where a resource that needs this resource's id can be created (using POST).""" - - property_name: str - post_path: str - error_code: int = 422 - - -@dataclass -class UniquePropertyValueConstraint(ResourceRelation): - """The value of the property must be unique within the resource scope.""" - - property_name: str - value: Any - error_code: int = 422 - - -@dataclass -class Dto(ABC): - """Base class for the Dto class.""" - - @staticmethod - def get_path_relations() -> list[PathPropertiesConstraint]: - """Return the list of Relations for the header and query parameters.""" - return [] - - def get_path_relations_for_error_code( - self, error_code: int - ) -> list[PathPropertiesConstraint]: - """Return the list of Relations associated with the given error_code.""" - relations: list[PathPropertiesConstraint] = [ - r - for r in self.get_path_relations() - if r.error_code == error_code - or ( - getattr(r, "invalid_value_error_code", None) == error_code - and getattr(r, "invalid_value", None) != NOT_SET - ) - ] - return relations - - @staticmethod - def get_parameter_relations() -> list[ResourceRelation]: - """Return the list of Relations for the header and query parameters.""" - return [] - - def get_parameter_relations_for_error_code( - self, error_code: int - ) -> list[ResourceRelation]: - """Return the list of Relations associated with the given error_code.""" - relations: list[ResourceRelation] = [ - r - for r in self.get_parameter_relations() - if r.error_code == error_code - or ( - getattr(r, "invalid_value_error_code", None) == error_code - and getattr(r, "invalid_value", None) != NOT_SET - ) - ] - return relations - - @staticmethod - def get_relations() -> list[ResourceRelation]: - """Return the list of Relations for the (json) body.""" - return [] - - def get_body_relations_for_error_code( - self, error_code: int - ) -> list[ResourceRelation]: - """ - Return the list of Relations associated with the given error_code that are - applicable to the body / payload of the request. - """ - relations: list[ResourceRelation] = [ - r - for r in self.get_relations() - if r.error_code == error_code - or ( - getattr(r, "invalid_value_error_code", None) == error_code - and getattr(r, "invalid_value", None) != NOT_SET - ) - ] - return relations - - def get_invalidated_data( - self, - schema: ObjectSchema, - status_code: int, - invalid_property_default_code: int, - ) -> dict[str, Any]: - """Return a data set with one of the properties set to an invalid value or type.""" - properties: dict[str, Any] = self.as_dict() - - relations = self.get_body_relations_for_error_code(error_code=status_code) - property_names = [r.property_name for r in relations] - if status_code == invalid_property_default_code: - # add all properties defined in the schema, including optional properties - property_names.extend((schema.properties.root.keys())) # type: ignore[union-attr] - if not property_names: - raise ValueError( - f"No property can be invalidated to cause status_code {status_code}" - ) - # Remove duplicates, then shuffle the property_names so different properties on - # the Dto are invalidated when rerunning the test. - shuffle(list(set(property_names))) - for property_name in property_names: - # if possible, invalidate a constraint but send otherwise valid data - id_dependencies = [ - r - for r in relations - if isinstance(r, IdDependency) and r.property_name == property_name - ] - if id_dependencies: - invalid_id = uuid4().hex - logger.debug( - f"Breaking IdDependency for status_code {status_code}: setting " - f"{property_name} to {invalid_id}" - ) - properties[property_name] = invalid_id - return properties - - invalid_value_from_constraint = [ - r.invalid_value - for r in relations - if isinstance(r, PropertyValueConstraint) - and r.property_name == property_name - and r.invalid_value_error_code == status_code - ] - if ( - invalid_value_from_constraint - and invalid_value_from_constraint[0] is not NOT_SET - ): - properties[property_name] = invalid_value_from_constraint[0] - logger.debug( - f"Using invalid_value {invalid_value_from_constraint[0]} to " - f"invalidate property {property_name}" - ) - return properties - - value_schema = schema.properties.root[property_name] # type: ignore[union-attr] - if isinstance(value_schema, UnionTypeSchema): - # Filter "type": "null" from the possible types since this indicates an - # optional / nullable property that can only be invalidated by sending - # invalid data of a non-null type - non_null_schemas = [ - s - for s in value_schema.resolved_schemas - if not isinstance(s, NullSchema) - ] - value_schema = choice(non_null_schemas) - - # there may not be a current_value when invalidating an optional property - current_value = properties.get(property_name, SENTINEL) - if current_value is SENTINEL: - # the current_value isn't very relevant as long as the type is correct - # so no logic to handle Relations / objects / arrays here - property_type = value_schema.type - if property_type == "object": - current_value = {} - elif property_type == "array": - current_value = [] - else: - current_value = value_schema.get_valid_value() - - values_from_constraint = [ - r.values[0] - for r in relations - if isinstance(r, PropertyValueConstraint) - and r.property_name == property_name - ] - - invalid_value = value_utils.get_invalid_value( - value_schema=value_schema, - current_value=current_value, - values_from_constraint=values_from_constraint, - ) - properties[property_name] = invalid_value - logger.debug( - f"Property {property_name} changed to {invalid_value!r} (received from " - f"get_invalid_value)" - ) - return properties - logger.warn("get_invalidated_data returned unchanged properties") - return properties # pragma: no cover - - def as_dict(self) -> dict[Any, Any]: - """Return the dict representation of the Dto.""" - result = {} - - for field in fields(self): - field_name = field.name - if field_name not in self.__dict__: - continue - original_name = get_oas_name_from_safe_name(field_name) - result[original_name] = getattr(self, field_name) - - return result diff --git a/src/OpenApiLibCore/dto_utils.py b/src/OpenApiLibCore/dto_utils.py deleted file mode 100644 index 4f8c8c9..0000000 --- a/src/OpenApiLibCore/dto_utils.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Module for helper methods and classes used by the openapi_executors module.""" - -from dataclasses import dataclass -from importlib import import_module -from typing import Any, Callable, Type, overload - -from robot.api import logger - -from OpenApiLibCore.dto_base import Dto -from OpenApiLibCore.protocols import ( - GetDtoClassType, - GetIdPropertyNameType, - GetPathDtoClassType, -) - - -@dataclass -class _DefaultIdPropertyName: - id_property_name: str = "id" - - -DEFAULT_ID_PROPERTY_NAME = _DefaultIdPropertyName() - - -@dataclass -class DefaultDto(Dto): - """A default Dto that can be instantiated.""" - - -def get_dto_class(mappings_module_name: str) -> GetDtoClassType: - return GetDtoClass(mappings_module_name=mappings_module_name) - - -class GetDtoClass: - """Callable class to return Dtos from user-implemented mappings file.""" - - def __init__(self, mappings_module_name: str) -> None: - try: - mappings_module = import_module(mappings_module_name) - self.dto_mapping: dict[tuple[str, str], Type[Dto]] = ( - mappings_module.DTO_MAPPING - ) - except (ImportError, AttributeError, ValueError) as exception: - if mappings_module_name != "no mapping": - logger.error(f"DTO_MAPPING was not imported: {exception}") - self.dto_mapping = {} - - def __call__(self, path: str, method: str) -> Type[Dto]: - try: - return self.dto_mapping[(path, method.lower())] - except KeyError: - logger.debug(f"No Dto mapping for {path} {method}.") - return DefaultDto - - -def get_path_dto_class(mappings_module_name: str) -> GetPathDtoClassType: - return GetPathDtoClass(mappings_module_name=mappings_module_name) - - -class GetPathDtoClass: - """Callable class to return Dtos from user-implemented mappings file.""" - - def __init__(self, mappings_module_name: str) -> None: - try: - mappings_module = import_module(mappings_module_name) - self.dto_mapping: dict[str, Type[Dto]] = mappings_module.PATH_MAPPING - except (ImportError, AttributeError, ValueError) as exception: - if mappings_module_name != "no mapping": - logger.error(f"PATH_MAPPING was not imported: {exception}") - self.dto_mapping = {} - - def __call__(self, path: str) -> Type[Dto]: - try: - return self.dto_mapping[path] - except KeyError: - logger.debug(f"No Dto mapping for {path}.") - return DefaultDto - - -def get_id_property_name(mappings_module_name: str) -> GetIdPropertyNameType: - return GetIdPropertyName(mappings_module_name=mappings_module_name) - - -class GetIdPropertyName: - """ - Callable class to return the name of the property that uniquely identifies - the resource from user-implemented mappings file. - """ - - def __init__(self, mappings_module_name: str) -> None: - try: - mappings_module = import_module(mappings_module_name) - self.id_mapping: dict[ - str, - str | tuple[str, Callable[[str], str] | Callable[[int], int]], - ] = mappings_module.ID_MAPPING - except (ImportError, AttributeError, ValueError) as exception: - if mappings_module_name != "no mapping": - logger.error(f"ID_MAPPING was not imported: {exception}") - self.id_mapping = {} - - def __call__( - self, path: str - ) -> tuple[str, Callable[[str], str] | Callable[[int], int]]: - try: - value_or_mapping = self.id_mapping[path] - if isinstance(value_or_mapping, str): - return (value_or_mapping, dummy_transformer) - return value_or_mapping - except KeyError: - default_id_name = DEFAULT_ID_PROPERTY_NAME.id_property_name - logger.debug(f"No id mapping for {path} ('{default_id_name}' will be used)") - return (default_id_name, dummy_transformer) - - -@overload -def dummy_transformer(valid_id: str) -> str: ... # pragma: no cover - - -@overload -def dummy_transformer(valid_id: int) -> int: ... # pragma: no cover - - -def dummy_transformer(valid_id: Any) -> Any: - return valid_id diff --git a/src/OpenApiLibCore/keyword_logic/__init__.py b/src/OpenApiLibCore/keyword_logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/OpenApiLibCore/path_functions.py b/src/OpenApiLibCore/keyword_logic/path_functions.py similarity index 75% rename from src/OpenApiLibCore/path_functions.py rename to src/OpenApiLibCore/keyword_logic/path_functions.py index d104d7b..13e5151 100644 --- a/src/OpenApiLibCore/path_functions.py +++ b/src/OpenApiLibCore/keyword_logic/path_functions.py @@ -3,18 +3,57 @@ import json as _json from itertools import zip_longest from random import choice -from typing import Any +from typing import Any, Literal, overload from requests import Response from robot.libraries.BuiltIn import BuiltIn -from OpenApiLibCore.models import OpenApiObject -from OpenApiLibCore.protocols import GetIdPropertyNameType, GetPathDtoClassType -from OpenApiLibCore.request_data import RequestData +from OpenApiLibCore.models import oas_models +from OpenApiLibCore.models.request_data import RequestData run_keyword = BuiltIn().run_keyword +@overload +def _run_keyword( + keyword_name: Literal["get_valid_id_for_path"], *args: str +) -> str: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_ids_from_url"], *args: str +) -> list[str]: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_valid_url"], *args: str +) -> str: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_parameterized_path_from_url"], *args: str +) -> str: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_request_data"], *args: str +) -> RequestData: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["authorized_request"], *args: object +) -> Response: ... # pragma: no cover + + +def _run_keyword(keyword_name: str, *args: object) -> object: + return run_keyword(keyword_name, *args) # pyright: ignore[reportArgumentType] + + def match_parts(parts: list[str], spec_parts: list[str]) -> bool: for part, spec_part in zip_longest(parts, spec_parts, fillvalue="Filler"): if part == "Filler" or spec_part == "Filler": @@ -24,14 +63,14 @@ def match_parts(parts: list[str], spec_parts: list[str]) -> bool: return True -def get_parametrized_path(path: str, openapi_spec: OpenApiObject) -> str: +def get_parametrized_path(path: str, openapi_spec: oas_models.OpenApiObject) -> str: path_parts = path.split("/") # if the last part is empty, the path has a trailing `/` that # should be ignored during matching if path_parts[-1] == "": _ = path_parts.pop(-1) - spec_paths: list[str] = list(openapi_spec.paths.keys()) + spec_paths = list(openapi_spec.paths.keys()) candidates: list[str] = [] @@ -63,19 +102,19 @@ def get_parametrized_path(path: str, openapi_spec: OpenApiObject) -> str: def get_valid_url( path: str, base_url: str, - get_path_dto_class: GetPathDtoClassType, - openapi_spec: OpenApiObject, + openapi_spec: oas_models.OpenApiObject, ) -> str: try: # path can be partially resolved or provided by a PathPropertiesConstraint parametrized_path = get_parametrized_path(path=path, openapi_spec=openapi_spec) - _ = openapi_spec.paths[parametrized_path] + path_item = openapi_spec.paths[parametrized_path] except KeyError: raise ValueError( f"{path} not found in paths section of the OpenAPI document." ) from None - dto_class = get_path_dto_class(path=path) - relations = dto_class.get_path_relations() + + relations_mapping = path_item.relations_mapping + relations = relations_mapping.get_path_relations() if relations_mapping else [] paths = [p.path for p in relations] if paths: url = f"{base_url}{choice(paths)}" @@ -85,9 +124,7 @@ def get_valid_url( if part.startswith("{") and part.endswith("}"): type_path_parts = path_parts[slice(index)] type_path = "/".join(type_path_parts) - existing_id: str | int | float = run_keyword( - "get_valid_id_for_path", type_path - ) + existing_id = _run_keyword("get_valid_id_for_path", type_path) path_parts[index] = str(existing_id) resolved_path = "/".join(path_parts) url = f"{base_url}{resolved_path}" @@ -96,14 +133,14 @@ def get_valid_url( def get_valid_id_for_path( path: str, - get_id_property_name: GetIdPropertyNameType, -) -> str | int: - url: str = run_keyword("get_valid_url", path) + openapi_spec: oas_models.OpenApiObject, +) -> str: + url = _run_keyword("get_valid_url", path) # Try to create a new resource to prevent conflicts caused by # operations performed on the same resource by other test cases - request_data: RequestData = run_keyword("get_request_data", path, "post") + request_data = _run_keyword("get_request_data", path, "post") - response: Response = run_keyword( + response = _run_keyword( "authorized_request", url, "post", @@ -112,13 +149,14 @@ def get_valid_id_for_path( request_data.get_required_properties_dict(), ) - id_property, id_transformer = get_id_property_name(path=path) + path_item = openapi_spec.paths[path] + id_property, id_transformer = path_item.id_mapper if not response.ok: # If a new resource cannot be created using POST, try to retrieve a # valid id using a GET request. try: - valid_id = choice(run_keyword("get_ids_from_url", url)) + valid_id = choice(_run_keyword("get_ids_from_url", url)) return id_transformer(valid_id) except Exception as exception: raise AssertionError( @@ -172,11 +210,11 @@ def get_valid_id_for_path( def get_ids_from_url( url: str, - get_id_property_name: GetIdPropertyNameType, + openapi_spec: oas_models.OpenApiObject, ) -> list[str]: - path: str = run_keyword("get_parameterized_path_from_url", url) - request_data: RequestData = run_keyword("get_request_data", path, "get") - response = run_keyword( + path = _run_keyword("get_parameterized_path_from_url", url) + request_data = _run_keyword("get_request_data", path, "get") + response = _run_keyword( "authorized_request", url, "get", @@ -187,11 +225,8 @@ def get_ids_from_url( response_data: dict[str, Any] | list[dict[str, Any]] = response.json() # determine the property name to use - mapping = get_id_property_name(path=path) - if isinstance(mapping, str): - id_property = mapping - else: - id_property, _ = mapping + path_item = openapi_spec.paths[path] + id_property, _ = path_item.id_mapper if isinstance(response_data, list): valid_ids: list[str] = [item[id_property] for item in response_data] diff --git a/src/OpenApiLibCore/path_invalidation.py b/src/OpenApiLibCore/keyword_logic/path_invalidation.py similarity index 56% rename from src/OpenApiLibCore/path_invalidation.py rename to src/OpenApiLibCore/keyword_logic/path_invalidation.py index 31cd042..97fb377 100644 --- a/src/OpenApiLibCore/path_invalidation.py +++ b/src/OpenApiLibCore/keyword_logic/path_invalidation.py @@ -1,24 +1,43 @@ """Module holding functions related to invalidation of paths and urls.""" from random import choice +from typing import Literal, overload from uuid import uuid4 from robot.libraries.BuiltIn import BuiltIn -from OpenApiLibCore.protocols import GetPathDtoClassType +from OpenApiLibCore.models import oas_models run_keyword = BuiltIn().run_keyword +@overload +def _run_keyword( + keyword_name: Literal["get_parameterized_path_from_url"], *args: str +) -> str: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["overload_default"], *args: object +) -> object: ... # pragma: no cover + + +def _run_keyword(keyword_name: str, *args: object) -> object: + return run_keyword(keyword_name, *args) # pyright: ignore[reportArgumentType] + + def get_invalidated_url( valid_url: str, - path: str, base_url: str, - get_path_dto_class: GetPathDtoClassType, + openapi_spec: oas_models.OpenApiObject, expected_status_code: int, ) -> str: - dto_class = get_path_dto_class(path=path) - relations = dto_class.get_path_relations() + path = _run_keyword("get_parameterized_path_from_url", valid_url) + path_item = openapi_spec.paths[path] + + relations_mapping = path_item.relations_mapping + relations = relations_mapping.get_path_relations() if relations_mapping else [] paths = [ p.invalid_value for p in relations @@ -27,7 +46,7 @@ def get_invalidated_url( if paths: url = f"{base_url}{choice(paths)}" return url - parameterized_path: str = run_keyword("get_parameterized_path_from_url", valid_url) + parameterized_path = _run_keyword("get_parameterized_path_from_url", valid_url) parameterized_url = base_url + parameterized_path valid_url_parts = list(reversed(valid_url.split("/"))) parameterized_parts = reversed(parameterized_url.split("/")) diff --git a/src/OpenApiLibCore/keyword_logic/resource_relations.py b/src/OpenApiLibCore/keyword_logic/resource_relations.py new file mode 100644 index 0000000..0648cfe --- /dev/null +++ b/src/OpenApiLibCore/keyword_logic/resource_relations.py @@ -0,0 +1,79 @@ +"""Module holding the functions related to relations between resources.""" + +from typing import Literal, overload + +from requests import Response +from robot.api import logger +from robot.libraries.BuiltIn import BuiltIn + +import OpenApiLibCore.keyword_logic.path_functions as _path_functions +from OpenApiLibCore.models.oas_models import OpenApiObject +from OpenApiLibCore.models.request_data import RequestData +from OpenApiLibCore.models.resource_relations import IdReference + +run_keyword = BuiltIn().run_keyword + + +@overload +def _run_keyword( + keyword_name: Literal["get_request_data"], *args: str +) -> RequestData: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_valid_url"], *args: str +) -> str: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["authorized_request"], *args: object +) -> Response: ... # pragma: no cover + + +def _run_keyword(keyword_name: str, *args: object) -> object: + return run_keyword(keyword_name, *args) # pyright: ignore[reportArgumentType] + + +def ensure_in_use( + url: str, + base_url: str, + openapi_spec: OpenApiObject, + resource_relation: IdReference, +) -> None: + resource_id = "" + + path = url.replace(base_url, "") + path_parts = path.split("/") + parameterized_path = _path_functions.get_parametrized_path( + path=path, openapi_spec=openapi_spec + ) + parameterized_path_parts = parameterized_path.split("/") + for part, param_part in zip( + reversed(path_parts), reversed(parameterized_path_parts) + ): + if param_part.endswith("}"): + resource_id = part + break + if not resource_id: + raise ValueError(f"The provided url ({url}) does not contain an id.") + request_data = _run_keyword("get_request_data", resource_relation.post_path, "post") + json_data = request_data.valid_data if request_data.valid_data else {} + # FIXME: currently only works for object / dict data + if isinstance(json_data, dict): + json_data[resource_relation.property_name] = resource_id + post_url = _run_keyword("get_valid_url", resource_relation.post_path) + response = _run_keyword( + "authorized_request", + post_url, + "post", + request_data.params, + request_data.headers, + json_data, + ) + if not response.ok: + logger.debug( + f"POST on {post_url} with json {json_data} failed: {response.json()}" + ) + response.raise_for_status() diff --git a/src/OpenApiLibCore/validation.py b/src/OpenApiLibCore/keyword_logic/validation.py similarity index 83% rename from src/OpenApiLibCore/validation.py rename to src/OpenApiLibCore/keyword_logic/validation.py index 4df33b5..b4e15eb 100644 --- a/src/OpenApiLibCore/validation.py +++ b/src/OpenApiLibCore/keyword_logic/validation.py @@ -3,7 +3,7 @@ import json as _json from enum import Enum from http import HTTPStatus -from typing import Any, Mapping +from typing import Any, Literal, Mapping, overload from openapi_core.contrib.requests import ( RequestsOpenAPIRequest, @@ -18,17 +18,52 @@ from robot.api.exceptions import Failure from robot.libraries.BuiltIn import BuiltIn -from OpenApiLibCore.models import ( +from OpenApiLibCore.annotations import JSON +from OpenApiLibCore.models.oas_models import ( OpenApiObject, ResponseObject, UnionTypeSchema, ) -from OpenApiLibCore.protocols import ResponseValidatorType -from OpenApiLibCore.request_data import RequestData, RequestValues +from OpenApiLibCore.models.request_data import RequestData, RequestValues +from OpenApiLibCore.protocols import IResponseValidator run_keyword = BuiltIn().run_keyword +@overload +def _run_keyword( + keyword_name: Literal["validate_response"], *args: object +) -> None: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["authorized_request"], *args: object +) -> Response: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_request_data"], *args: str +) -> RequestData: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["validate_send_response"], *args: Response | JSON +) -> None: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["assert_href_to_resource_is_valid"], *args: str | JSON +) -> None: ... # pragma: no cover + + +def _run_keyword(keyword_name: str, *args: object) -> object: + return run_keyword(keyword_name, *args) # pyright: ignore[reportArgumentType] + + class ValidationLevel(str, Enum): """The available levels for the response_validation parameter.""" @@ -44,7 +79,7 @@ def perform_validated_request( request_values: RequestValues, original_data: Mapping[str, Any], ) -> None: - response = run_keyword( + response = _run_keyword( "authorized_request", request_values.url, request_values.method, @@ -78,13 +113,13 @@ def perform_validated_request( f"Response status_code {response.status_code} was not {status_code}." ) - run_keyword("validate_response", path, response, original_data) + _run_keyword("validate_response", path, response, original_data) if request_values.method == "DELETE": - request_data: RequestData = run_keyword("get_request_data", path, "GET") + request_data = _run_keyword("get_request_data", path, "GET") get_params = request_data.params get_headers = request_data.headers - get_response = run_keyword( + get_response = _run_keyword( "authorized_request", request_values.url, "GET", get_params, get_headers ) if response.ok: @@ -109,13 +144,13 @@ def perform_validated_request( def validate_response( path: str, response: Response, - response_validator: ResponseValidatorType, + response_validator: IResponseValidator, server_validation_warning_logged: bool, disable_server_validation: bool, - invalid_property_default_response: int, + invalid_data_default_response: int, response_validation: str, openapi_spec: OpenApiObject, - original_data: Mapping[str, Any], + original_data: JSON, ) -> None: if response.status_code == int(HTTPStatus.NO_CONTENT): assert not response.content @@ -127,7 +162,7 @@ def validate_response( response_validator=response_validator, server_validation_warning_logged=server_validation_warning_logged, disable_server_validation=disable_server_validation, - invalid_property_default_response=invalid_property_default_response, + invalid_data_default_response=invalid_data_default_response, response_validation=response_validation, ) except OpenAPIError as exception: @@ -188,24 +223,24 @@ def validate_response( # ensure the href is valid if the response is an object that contains a href if isinstance(json_response, dict): if href := json_response.get("href"): - run_keyword("assert_href_to_resource_is_valid", href, json_response) + _run_keyword("assert_href_to_resource_is_valid", href, json_response) # every property that was sucessfully send and that is in the response # schema must have the value that was send if response.ok and response.request.method in ["POST", "PUT", "PATCH"]: - run_keyword("validate_send_response", response, original_data) + _run_keyword("validate_send_response", response, original_data) return None def assert_href_to_resource_is_valid( - href: str, origin: str, base_url: str, referenced_resource: dict[str, Any] + href: str, origin: str, base_url: str, referenced_resource: JSON ) -> None: url = f"{origin}{href}" path = url.replace(base_url, "") - request_data: RequestData = run_keyword("get_request_data", path, "GET") + request_data = _run_keyword("get_request_data", path, "GET") params = request_data.params headers = request_data.headers - get_response = run_keyword("authorized_request", url, "GET", params, headers) + get_response = _run_keyword("authorized_request", url, "GET", params, headers) assert get_response.json() == referenced_resource, ( f"{get_response.json()} not equal to original {referenced_resource}" ) @@ -299,6 +334,9 @@ def validate_dict_response( if original_data: for send_property_name, send_value in original_data.items(): if send_property_name not in send_json.keys(): + if send_property_name not in response_data: + logger.debug(f"'{send_property_name}' not found in response data.") + continue assert send_value == response_data[send_property_name], ( f"Received value for {send_property_name} '{response_data[send_property_name]}' does not " f"match '{send_value}' in the pre-patch data" @@ -310,7 +348,7 @@ def validate_dict_response( def validate_response_using_validator( response: Response, - response_validator: ResponseValidatorType, + response_validator: IResponseValidator, ) -> None: openapi_request = RequestsOpenAPIRequest(response.request) openapi_response = RequestsOpenAPIResponse(response) @@ -319,13 +357,23 @@ def validate_response_using_validator( def _validate_response( response: Response, - response_validator: ResponseValidatorType, + response_validator: IResponseValidator, server_validation_warning_logged: bool, disable_server_validation: bool, - invalid_property_default_response: int, + invalid_data_default_response: int, response_validation: str, ) -> None: try: + content_type = response.headers.get("Content-Type", "") + if content_type: + key_value = "Content-Type" + else: + content_type = response.headers.get("content-type", "") + if content_type: + key_value = "content-type" + if "json" in content_type.lower(): + content_type, _, _ = content_type.partition(";") + response.headers.update({key_value: content_type}) # pyright: ignore[reportPossiblyUnboundVariable] validate_response_using_validator( response=response, response_validator=response_validator, @@ -354,7 +402,7 @@ def _validate_response( if disable_server_validation: return - if response.status_code == invalid_property_default_response: + if response.status_code == invalid_data_default_response: logger.debug(error_message) return if response_validation == ValidationLevel.STRICT: @@ -372,7 +420,7 @@ def _get_response_object( method = method.lower() status = str(status_code) path_item = openapi_spec.paths[path] - path_operations = path_item.get_operations() + path_operations = path_item.operations operation_data = path_operations.get(method) if operation_data is None: raise ValueError(f"method '{method}' not supported for {path}.") diff --git a/src/OpenApiLibCore/models.py b/src/OpenApiLibCore/models.py deleted file mode 100644 index e37d9c0..0000000 --- a/src/OpenApiLibCore/models.py +++ /dev/null @@ -1,759 +0,0 @@ -import base64 -from abc import abstractmethod -from collections import ChainMap -from functools import cached_property -from random import choice, randint, uniform -from sys import float_info -from typing import ( - Generator, - Generic, - Literal, - Mapping, - TypeAlias, - TypeVar, -) - -import rstr -from pydantic import BaseModel, Field, RootModel -from robot.api import logger - -from OpenApiLibCore.annotations import JSON -from OpenApiLibCore.localized_faker import FAKE, fake_string - -EPSILON = float_info.epsilon - -O = TypeVar("O") - - -class SchemaBase(BaseModel, Generic[O], frozen=True): - readOnly: bool = False - writeOnly: bool = False - - @abstractmethod - def get_valid_value(self) -> JSON: ... - - @abstractmethod - def get_values_out_of_bounds(self, current_value: O) -> list[O]: ... - - @abstractmethod - def get_invalid_value_from_const_or_enum(self) -> O: ... - - -class NullSchema(SchemaBase[None], frozen=True): - type: Literal["null"] = "null" - - def get_valid_value(self) -> None: - return None - - def get_values_out_of_bounds(self, current_value: None) -> list[None]: - raise ValueError - - def get_invalid_value_from_const_or_enum(self) -> None: - raise ValueError - - @property - def can_be_invalidated(self) -> bool: - return False - - @property - def annotation_string(self) -> str: - return "None" - - -class BooleanSchema(SchemaBase[bool], frozen=True): - type: Literal["boolean"] = "boolean" - const: bool | None = None - nullable: bool = False - - def get_valid_value(self) -> bool: - if self.const is not None: - return self.const - return choice([True, False]) - - def get_values_out_of_bounds(self, current_value: bool) -> list[bool]: - raise ValueError - - def get_invalid_value_from_const_or_enum(self) -> bool: - if self.const is not None: - return not self.const - raise ValueError - - @property - def can_be_invalidated(self) -> bool: - return True - - @property - def annotation_string(self) -> str: - return "bool" - - -class StringSchema(SchemaBase[str], frozen=True): - type: Literal["string"] = "string" - format: str = "" - pattern: str = "" - maxLength: int | None = None - minLength: int | None = None - const: str | None = None - enum: list[str] | None = None - nullable: bool = False - - def get_valid_value(self) -> bytes | str: - """Generate a random string within the min/max length in the schema, if specified.""" - if self.const is not None: - return self.const - if self.enum is not None: - return choice(self.enum) - # if a pattern is provided, format and min/max length can be ignored - if pattern := self.pattern: - try: - return rstr.xeger(pattern) - except Exception as exception: - logger.warn( - f"An error occured trying to generate a string matching the " - f"pattern defined in the specification. To ensure a valid value " - f"is generated for this property, a PropertyValueConstraint can be " - f"configured. See the Advanced Use section of the OpenApiTools " - f"documentation for more details." - f"\nThe exception was: {exception}\nThe pattern was: {pattern}" - ) - minimum = self.minLength if self.minLength is not None else 0 - maximum = self.maxLength if self.maxLength is not None else 36 - maximum = max(minimum, maximum) - format_ = self.format if self.format else "uuid" - # byte is a special case due to the required encoding - if format_ == "byte": - data = FAKE.uuid() - return base64.b64encode(data.encode("utf-8")) - value = fake_string(string_format=format_) - while len(value) < minimum: - # use fake.name() to ensure the returned string uses the provided locale - value = value + FAKE.name() - if len(value) > maximum: - value = value[:maximum] - return value - - def get_values_out_of_bounds(self, current_value: str) -> list[str]: - invalid_values: list[str] = [] - if self.minLength: - invalid_values.append(current_value[0 : self.minLength - 1]) - # if there is a maximum length, send 1 character more - if self.maxLength: - invalid_string_value = current_value if current_value else "x" - # add random characters from the current value to prevent adding new characters - while len(invalid_string_value) <= self.maxLength: - invalid_string_value += choice(invalid_string_value) - invalid_values.append(invalid_string_value) - if invalid_values: - return invalid_values - raise ValueError - - def get_invalid_value_from_const_or_enum(self) -> str: - valid_values = [] - if self.const is not None: - valid_values = [self.const] - if self.enum is not None: - valid_values = self.enum - - if not valid_values: - raise ValueError - - invalid_value = "" - for value in valid_values: - invalid_value += value + value - - return invalid_value - - @property - def can_be_invalidated(self) -> bool: - if ( - self.maxLength is not None - or self.minLength is not None - or self.const is not None - or self.enum is not None - ): - return True - return False - - @property - def annotation_string(self) -> str: - return "str" - - -class IntegerSchema(SchemaBase[int], frozen=True): - type: Literal["integer"] = "integer" - format: str = "int32" - maximum: int | None = None - exclusiveMaximum: int | bool | None = None - minimum: int | None = None - exclusiveMinimum: int | bool | None = None - multipleOf: int | None = None # TODO: implement support - const: int | None = None - enum: list[int] | None = None - nullable: bool = False - - @cached_property - def _max_int(self) -> int: - if self.format == "int64": - return 9223372036854775807 - return 2147483647 - - @cached_property - def _min_int(self) -> int: - if self.format == "int64": - return -9223372036854775808 - return -2147483648 - - @cached_property - def _max_value(self) -> int: - # OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum - # OAS 3.1: exclusiveMinimum/Maximum is an integer - if isinstance(self.exclusiveMaximum, int) and not isinstance( - self.exclusiveMaximum, bool - ): - return self.exclusiveMaximum - 1 - - if isinstance(self.maximum, int): - if self.exclusiveMaximum is True: - return self.maximum - 1 - return self.maximum - - return self._max_int - - @cached_property - def _min_value(self) -> int: - # OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum - # OAS 3.1: exclusiveMinimum/Maximum is an integer - if isinstance(self.exclusiveMinimum, int) and not isinstance( - self.exclusiveMinimum, bool - ): - return self.exclusiveMinimum + 1 - - if isinstance(self.minimum, int): - if self.exclusiveMinimum is True: - return self.minimum + 1 - return self.minimum - - return self._min_int - - def get_valid_value(self) -> int: - """Generate a random int within the min/max range of the schema, if specified.""" - if self.const is not None: - return self.const - if self.enum is not None: - return choice(self.enum) - - return randint(self._min_value, self._max_value) - - def get_values_out_of_bounds(self, current_value: int) -> list[int]: # pylint: disable=unused-argument - invalid_values: list[int] = [] - - if self._min_value > self._min_int: - invalid_values.append(self._min_value - 1) - - if self._max_value < self._max_int: - invalid_values.append(self._max_value + 1) - - if invalid_values: - return invalid_values - - raise ValueError - - def get_invalid_value_from_const_or_enum(self) -> int: - valid_values = [] - if self.const is not None: - valid_values = [self.const] - if self.enum is not None: - valid_values = self.enum - - if not valid_values: - raise ValueError - - invalid_value = 0 - for value in valid_values: - invalid_value += abs(value) + abs(value) - - return invalid_value - - @property - def can_be_invalidated(self) -> bool: - return True - - @property - def annotation_string(self) -> str: - return "int" - - -class NumberSchema(SchemaBase[float], frozen=True): - type: Literal["number"] = "number" - maximum: int | float | None = None - exclusiveMaximum: int | float | bool | None = None - minimum: int | float | None = None - exclusiveMinimum: int | float | bool | None = None - multipleOf: int | None = None # TODO: implement support - const: int | float | None = None - enum: list[int | float] | None = None - nullable: bool = False - - @cached_property - def _max_float(self) -> float: - return 9223372036854775807.0 - - @cached_property - def _min_float(self) -> float: - return -9223372036854775808.0 - - @cached_property - def _max_value(self) -> float: - # OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum - # OAS 3.1: exclusiveMinimum/Maximum is an integer or a float - if isinstance(self.exclusiveMaximum, (int, float)) and not isinstance( - self.exclusiveMaximum, bool - ): - return self.exclusiveMaximum - 0.0000000001 - - if isinstance(self.maximum, (int, float)): - if self.exclusiveMaximum is True: - return self.maximum - 0.0000000001 - return self.maximum - - return self._max_float - - @cached_property - def _min_value(self) -> float: - # OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum - # OAS 3.1: exclusiveMinimum/Maximum is an integer or a float - if isinstance(self.exclusiveMinimum, (int, float)) and not isinstance( - self.exclusiveMinimum, bool - ): - return self.exclusiveMinimum + 0.0000000001 - - if isinstance(self.minimum, (int, float)): - if self.exclusiveMinimum is True: - return self.minimum + 0.0000000001 - return self.minimum - - return self._min_float - - def get_valid_value(self) -> float: - """Generate a random float within the min/max range of the schema, if specified.""" - if self.const is not None: - return self.const - if self.enum is not None: - return choice(self.enum) - - return uniform(self._min_value, self._max_value) - - def get_values_out_of_bounds(self, current_value: float) -> list[float]: # pylint: disable=unused-argument - invalid_values: list[float] = [] - - if self._min_value > self._min_float: - invalid_values.append(self._min_value - 0.000000001) - - if self._max_value < self._max_float: - invalid_values.append(self._max_value + 0.000000001) - - if invalid_values: - return invalid_values - - raise ValueError - - def get_invalid_value_from_const_or_enum(self) -> float: - valid_values = [] - if self.const is not None: - valid_values = [self.const] - if self.enum is not None: - valid_values = self.enum - - if not valid_values: - raise ValueError - - invalid_value = 0.0 - for value in valid_values: - invalid_value += abs(value) + abs(value) - - return invalid_value - - @property - def can_be_invalidated(self) -> bool: - return True - - @property - def annotation_string(self) -> str: - return "float" - - -class ArraySchema(SchemaBase[list[JSON]], frozen=True): - type: Literal["array"] = "array" - items: "SchemaObjectTypes" - maxItems: int | None = None - minItems: int | None = None - uniqueItems: bool = False - const: list[JSON] | None = None - enum: list[list[JSON]] | None = None - nullable: bool = False - - def get_valid_value(self) -> list[JSON]: - if self.const is not None: - return self.const - - if self.enum is not None: - return choice(self.enum) - - minimum = self.minItems if self.minItems is not None else 0 - maximum = self.maxItems if self.maxItems is not None else 1 - maximum = max(minimum, maximum) - - value: list[JSON] = [] - for _ in range(maximum): - item_value = self.items.get_valid_value() - value.append(item_value) - return value - - def get_values_out_of_bounds(self, current_value: list[JSON]) -> list[list[JSON]]: - invalid_values: list[list[JSON]] = [] - - if self.minItems: - invalid_value = current_value[0 : self.minItems - 1] - invalid_values.append(invalid_value) - - if self.maxItems is not None: - invalid_value = [] - if not current_value: - current_value = self.get_valid_value() - - if not current_value: - current_value = [self.items.get_valid_value()] - - while len(invalid_value) <= self.maxItems: - invalid_value.append(choice(current_value)) - invalid_values.append(invalid_value) - - if invalid_values: - return invalid_values - - raise ValueError - - def get_invalid_value_from_const_or_enum(self) -> list[JSON]: - valid_values = [] - if self.const is not None: - valid_values = [self.const] - if self.enum is not None: - valid_values = self.enum - - if not valid_values: - raise ValueError - - invalid_value = [] - for value in valid_values: - invalid_value.extend(value) - invalid_value.extend(value) - - return invalid_value - - @property - def can_be_invalidated(self) -> bool: - if ( - self.maxItems is not None - or self.minItems is not None - or self.uniqueItems - or self.const is not None - or self.enum is not None - ): - return True - if isinstance(self.items, (BooleanSchema, IntegerSchema, NumberSchema)): - return True - return False - - @property - def annotation_string(self) -> str: - return f"list[{self.items.annotation_string}]" - - -# NOTE: Workaround for cyclic PropertiesMapping / SchemaObjectTypes annotations -def _get_properties_mapping_default() -> "PropertiesMapping": - return _get_empty_properties_mapping() - - -class ObjectSchema(SchemaBase[dict[str, JSON]], frozen=True): - type: Literal["object"] = "object" - properties: "PropertiesMapping" = Field( - default_factory=_get_properties_mapping_default - ) - additionalProperties: "bool | SchemaObjectTypes" = True - required: list[str] = [] - maxProperties: int | None = None - minProperties: int | None = None - const: dict[str, JSON] | None = None - enum: list[dict[str, JSON]] | None = None - nullable: bool = False - - def get_valid_value(self) -> dict[str, JSON]: - raise NotImplementedError - - def get_values_out_of_bounds( - self, current_value: Mapping[str, JSON] - ) -> list[dict[str, JSON]]: - raise ValueError - - def get_invalid_value_from_const_or_enum(self) -> dict[str, JSON]: - valid_values = [] - if self.const is not None: - valid_values = [self.const] - if self.enum is not None: - valid_values = self.enum - - if not valid_values: - raise ValueError - - # This invalidation will not work for a const and may not work for - # an enum. In that case a different invalidation approach will be used. - invalid_value = {**valid_values[0]} - for value in valid_values: - for key in invalid_value.keys(): - invalid_value[key] = value.get(key) - if invalid_value not in valid_values: - return invalid_value - - raise ValueError - - @property - def can_be_invalidated(self) -> bool: - if ( - self.required - or self.maxProperties is not None - or self.minProperties is not None - or self.const is not None - or self.enum is not None - ): - return True - return False - - @property - def annotation_string(self) -> str: - return "dict[str, Any]" - - -ResolvedSchemaObjectTypes: TypeAlias = ( - NullSchema - | BooleanSchema - | StringSchema - | IntegerSchema - | NumberSchema - | ArraySchema - | ObjectSchema -) - - -class UnionTypeSchema(SchemaBase[JSON], frozen=True): - allOf: list["SchemaObjectTypes"] = [] - anyOf: list["SchemaObjectTypes"] = [] - oneOf: list["SchemaObjectTypes"] = [] - - def get_valid_value(self) -> JSON: - chosen_schema = choice(self.resolved_schemas) - return chosen_schema.get_valid_value() - - def get_values_out_of_bounds(self, current_value: JSON) -> list[JSON]: - raise ValueError - - @property - def resolved_schemas(self) -> list[ResolvedSchemaObjectTypes]: - return list(self._get_resolved_schemas()) - - def _get_resolved_schemas(self) -> Generator[ResolvedSchemaObjectTypes, None, None]: - if self.allOf: - properties_list: list[PropertiesMapping] = [] - additional_properties_list = [] - required_list = [] - max_properties_list = [] - min_properties_list = [] - nullable_list = [] - - for schema in self.allOf: - if not isinstance(schema, ObjectSchema): - raise NotImplementedError("allOf only supported for ObjectSchemas") - - if schema.const is not None: - raise ValueError("allOf and models with a const are not compatible") - - if schema.enum: - raise ValueError("allOf and models with enums are not compatible") - - if schema.properties: - properties_list.append(schema.properties) - additional_properties_list.append(schema.additionalProperties) - required_list += schema.required - max_properties_list.append(schema.maxProperties) - min_properties_list.append(schema.minProperties) - nullable_list.append(schema.nullable) - - properties_dicts = [mapping.root for mapping in properties_list] - properties = dict(ChainMap(*properties_dicts)) - - if True in additional_properties_list: - additional_properties_value: bool | SchemaObjectTypes = True - else: - additional_properties_types = [] - for additional_properties_item in additional_properties_list: - if isinstance( - additional_properties_item, ResolvedSchemaObjectTypes - ): - additional_properties_types.append(additional_properties_item) - if not additional_properties_types: - additional_properties_value = False - else: - additional_properties_value = UnionTypeSchema( - anyOf=additional_properties_types, - ) - - max_properties = [max for max in max_properties_list if max is not None] - min_properties = [min for min in min_properties_list if min is not None] - max_propeties_value = max(max_properties) if max_properties else None - min_propeties_value = min(min_properties) if min_properties else None - - merged_schema = ObjectSchema( - type="object", - properties=properties, - additionalProperties=additional_properties_value, - required=required_list, - maxProperties=max_propeties_value, - minProperties=min_propeties_value, - nullable=all(nullable_list), - ) - yield merged_schema - else: - for schema in self.anyOf + self.oneOf: - if isinstance(schema, ResolvedSchemaObjectTypes): - yield schema - else: - yield from schema.resolved_schemas - - def get_invalid_value_from_const_or_enum(self) -> JSON: - raise ValueError - - @property - def annotation_string(self) -> str: - unique_annotations = {s.annotation_string for s in self.resolved_schemas} - return " | ".join(unique_annotations) - - -SchemaObjectTypes: TypeAlias = ResolvedSchemaObjectTypes | UnionTypeSchema - - -class PropertiesMapping(RootModel[dict[str, "SchemaObjectTypes"]], frozen=True): ... - - -def _get_empty_properties_mapping() -> PropertiesMapping: - return PropertiesMapping(root={}) - - -class ParameterObject(BaseModel): - name: str - in_: str = Field(..., alias="in") - required: bool = False - description: str = "" - schema_: SchemaObjectTypes | None = Field(None, alias="schema") - - -class MediaTypeObject(BaseModel): - schema_: SchemaObjectTypes | None = Field(None, alias="schema") - - -class RequestBodyObject(BaseModel): - content: dict[str, MediaTypeObject] - required: bool = False - description: str = "" - - @cached_property - def schema_(self) -> SchemaObjectTypes | None: - if not self.mime_type: - return None - - if len(self._json_schemas) > 1: - logger.info( - f"Multiple JSON media types defined for requestBody, " - f"using the first candidate from {self.content}" - ) - return self._json_schemas[self.mime_type] - - @cached_property - def mime_type(self) -> str | None: - if not self._json_schemas: - return None - - return next(iter(self._json_schemas)) - - @cached_property - def _json_schemas(self) -> dict[str, SchemaObjectTypes]: - json_schemas = { - mime_type: media_type.schema_ - for mime_type, media_type in self.content.items() - if "json" in mime_type and media_type.schema_ is not None - } - return json_schemas - - -class HeaderObject(BaseModel): ... - - -class LinkObject(BaseModel): ... - - -class ResponseObject(BaseModel): - description: str - content: dict[str, MediaTypeObject] = {} - headers: dict[str, HeaderObject] = {} - links: dict[str, LinkObject] = {} - - -class OperationObject(BaseModel): - operationId: str | None = None - summary: str = "" - description: str = "" - tags: list[str] = [] - parameters: list[ParameterObject] = [] - requestBody: RequestBodyObject | None = None - responses: dict[str, ResponseObject] = {} - - def update_parameters(self, parameters: list[ParameterObject]) -> None: - self.parameters.extend(parameters) - - -class PathItemObject(BaseModel): - get: OperationObject | None = None - post: OperationObject | None = None - patch: OperationObject | None = None - put: OperationObject | None = None - delete: OperationObject | None = None - summary: str = "" - description: str = "" - parameters: list[ParameterObject] = [] - - def get_operations(self) -> dict[str, OperationObject]: - return { - k: v for k, v in self.__dict__.items() if isinstance(v, OperationObject) - } - - def update_operation_parameters(self) -> None: - if not self.parameters: - return - - operations_to_update = self.get_operations() - for operation_object in operations_to_update.values(): - operation_object.update_parameters(self.parameters) - - -class InfoObject(BaseModel): - title: str - version: str - summary: str = "" - description: str = "" - - -class OpenApiObject(BaseModel): - info: InfoObject - paths: dict[str, PathItemObject] - - def model_post_init(self, context: object) -> None: - for path_object in self.paths.values(): - path_object.update_operation_parameters() diff --git a/src/OpenApiLibCore/models/__init__.py b/src/OpenApiLibCore/models/__init__.py new file mode 100644 index 0000000..dd35889 --- /dev/null +++ b/src/OpenApiLibCore/models/__init__.py @@ -0,0 +1,17 @@ +class Ignore: + """Helper class to flag properties to be ignored in data generation.""" + + def __str__(self) -> str: + return "IGNORE" # pragma: no cover + + +class UnSet: + """Helper class to flag arguments that have not been set in a keyword call.""" + + def __str__(self) -> str: + return "UNSET" # pragma: no cover + + +IGNORE = Ignore() + +UNSET = UnSet() diff --git a/src/OpenApiLibCore/models/oas_models.py b/src/OpenApiLibCore/models/oas_models.py new file mode 100644 index 0000000..cddc927 --- /dev/null +++ b/src/OpenApiLibCore/models/oas_models.py @@ -0,0 +1,1438 @@ +from __future__ import annotations + +import builtins +from abc import abstractmethod +from collections import ChainMap +from copy import deepcopy +from functools import cached_property +from random import choice, randint, sample, shuffle, uniform +from sys import float_info +from typing import ( + Annotated, + Any, + Callable, + Generator, + Generic, + Iterable, + Literal, + Mapping, + TypeAlias, + TypeGuard, + TypeVar, + Union, + cast, +) +from uuid import uuid4 + +import rstr +from pydantic import BaseModel, Field, RootModel +from robot.api import logger +from robot.libraries.BuiltIn import BuiltIn + +from OpenApiLibCore.annotations import JSON +from OpenApiLibCore.data_generation.localized_faker import FAKE, fake_string +from OpenApiLibCore.data_generation.value_utils import ( + json_type_name_of_python_type, + python_type_by_json_type_name, +) +from OpenApiLibCore.data_relations.relations_base import RelationsMapping +from OpenApiLibCore.models import IGNORE, Ignore +from OpenApiLibCore.models.resource_relations import ( + NOT_SET, + IdDependency, + PropertyValueConstraint, +) +from OpenApiLibCore.protocols import RelationsMappingType +from OpenApiLibCore.utils.id_mapping import dummy_transformer +from OpenApiLibCore.utils.parameter_utils import get_safe_name_for_oas_name + +run_keyword = BuiltIn().run_keyword + +EPSILON = float_info.epsilon + +SENTINEL = object() + +O = TypeVar("O") +AI = TypeVar("AI", bound=JSON) + + +def is_object_schema(schema: SchemaObjectTypes) -> TypeGuard[ObjectSchema]: + return isinstance(schema, ObjectSchema) + + +class SchemaBase(BaseModel, Generic[O], frozen=True): + readOnly: bool = False + writeOnly: bool = False + relations_mapping: RelationsMappingType = RelationsMapping # type: ignore[assignment] + + @abstractmethod + def get_valid_value( + self, + operation_id: str | None = None, + ) -> tuple[O, SchemaObjectTypes]: ... + + @abstractmethod + def get_values_out_of_bounds(self, current_value: O) -> list[O]: ... + + @abstractmethod + def get_invalid_value_from_const_or_enum(self) -> O: ... + + @abstractmethod + def get_invalid_value_from_constraint(self, values_from_constraint: list[O]) -> O: + """ + Return a value of the same type as the values in the values_from_constraints that + is not in the values_from_constraints, if possible. Otherwise raise ValueError. + """ + + def get_invalid_value( + self, + valid_value: O, + values_from_constraint: Iterable[O] = tuple(), + ) -> O | str | list[JSON] | Ignore: + """Return a random value that violates the provided value_schema.""" + invalid_values: list[O | str | list[JSON] | Ignore] = [] + value_type = getattr(self, "type") + + if not isinstance(valid_value, python_type_by_json_type_name(value_type)): + valid_value = self.get_valid_value()[0] + + if values_from_constraint: + # if IGNORE is in the values_from_constraints, the parameter needs to be + # ignored for an OK response so leaving the value at it's original value + # should result in the specified error response + if any(map(lambda x: isinstance(x, Ignore), values_from_constraint)): + return IGNORE + try: + return self.get_invalid_value_from_constraint( + values_from_constraint=list(values_from_constraint), + ) + except ValueError: + pass + + # For schemas with a const or enum, add invalidated values from those + try: + invalid_value = self.get_invalid_value_from_const_or_enum() + invalid_values.append(invalid_value) + except ValueError: + pass + + # Violate min / max values or length if possible + try: + values_out_of_bounds = self.get_values_out_of_bounds( + current_value=valid_value + ) + invalid_values += values_out_of_bounds + except ValueError: + pass + + # No value constraints or min / max ranges to violate, so change the data type + if value_type == "string": + # Since int / float / bool can always be cast to sting, change + # the string to a nested object. + # An array gets exploded in query strings, "null" is then often invalid + invalid_values.append([{"invalid": [None, False]}, "null", None, True]) + else: + invalid_values.append(FAKE.uuid()) + + return choice(invalid_values) + + def attach_relations_mapping(self, relations_mapping: RelationsMappingType) -> None: + # NOTE: https://github.com/pydantic/pydantic/issues/11495 + self.__dict__["relations_mapping"] = relations_mapping + + +class NullSchema(SchemaBase[None], frozen=True): + type: Literal["null"] = "null" + nullable: bool = False + + def get_valid_value( + self, + operation_id: str | None = None, + ) -> tuple[None, NullSchema]: + return None, self + + def get_values_out_of_bounds(self, current_value: None) -> list[None]: + raise ValueError + + def get_invalid_value_from_const_or_enum(self) -> None: + raise ValueError + + def get_invalid_value_from_constraint( + self, values_from_constraint: list[None] + ) -> None: + raise ValueError + + @property + def can_be_invalidated(self) -> bool: + return False + + @property + def annotation_string(self) -> str: + return "None" + + @property + def python_type(self) -> builtins.type: + return type(None) + + +class BooleanSchema(SchemaBase[bool], frozen=True): + type: Literal["boolean"] = "boolean" + const: bool | None = None + nullable: bool = False + + def get_valid_value( + self, + operation_id: str | None = None, + ) -> tuple[bool, BooleanSchema]: + if self.const is not None: + return self.const, self + return choice([True, False]), self + + def get_values_out_of_bounds(self, current_value: bool) -> list[bool]: + raise ValueError + + def get_invalid_value_from_const_or_enum(self) -> bool: + if self.const is not None: + return not self.const + raise ValueError + + def get_invalid_value_from_constraint( + self, values_from_constraint: list[bool] + ) -> bool: + if len(values_from_constraint) == 1: + return not values_from_constraint[0] + raise ValueError + + @property + def can_be_invalidated(self) -> bool: + return True + + @property + def annotation_string(self) -> str: + return "bool" + + @property + def python_type(self) -> builtins.type: + return bool + + +class StringSchema(SchemaBase[str], frozen=True): + type: Literal["string"] = "string" + format: str = "" + pattern: str = "" + maxLength: int | None = None + minLength: int | None = None + const: str | None = None + enum: list[str] | None = None + nullable: bool = False + + def get_valid_value( + self, + operation_id: str | None = None, + ) -> tuple[str, StringSchema]: + """Generate a random string within the min/max length in the schema, if specified.""" + if self.const is not None: + return self.const, self + if self.enum is not None: + return choice(self.enum), self + # if a pattern is provided, format and min/max length can be ignored + if pattern := self.pattern: + try: + return rstr.xeger(pattern), self + except Exception as exception: + logger.warn( + f"An error occured trying to generate a string matching the " + f"pattern defined in the specification. To ensure a valid value " + f"is generated for this property, a PropertyValueConstraint can be " + f"configured. See the Advanced Use section of the OpenApiTools " + f"documentation for more details." + f"\nThe exception was: {exception}\nThe pattern was: {pattern}" + ) + minimum = self.minLength if self.minLength is not None else 0 + maximum = self.maxLength if self.maxLength is not None else 36 + maximum = max(minimum, maximum) + + format_ = self.format if self.format else "uuid" + value = fake_string(string_format=format_) + while len(value) < minimum: + value = value + fake_string(string_format=format_) + if len(value) > maximum: + value = value[:maximum] + return value, self + + def get_values_out_of_bounds(self, current_value: str) -> list[str]: + invalid_values: list[str] = [] + if self.minLength: + invalid_values.append(current_value[0 : self.minLength - 1]) + # if there is a maximum length, send 1 character more + if self.maxLength: + invalid_value = current_value if current_value else "x" + # add random characters from the current value to prevent adding new characters + while len(invalid_value) <= self.maxLength: + invalid_value += choice(invalid_value) + invalid_values.append(invalid_value) + if invalid_values: + return invalid_values + raise ValueError + + def get_invalid_value_from_const_or_enum(self) -> str: + valid_values = [] + if self.const is not None: + valid_values = [self.const] + if self.enum is not None: + valid_values = self.enum + + if not valid_values: + raise ValueError + + invalid_value = "" + for value in valid_values: + invalid_value += value + value + + return invalid_value + + def get_invalid_value_from_constraint( + self, values_from_constraint: list[str] + ) -> str: + invalid_values = 2 * values_from_constraint + invalid_value = invalid_values.pop() + for value in invalid_values: + invalid_value = invalid_value + value + + if not invalid_value: + raise ValueError("Value invalidation yielded an empty string.") + return invalid_value + + @property + def can_be_invalidated(self) -> bool: + if ( + self.maxLength is not None + or self.minLength is not None + or self.const is not None + or self.enum is not None + ): + return True + return False + + @property + def annotation_string(self) -> str: + return "str" + + @property + def python_type(self) -> builtins.type: + return str + + +class IntegerSchema(SchemaBase[int], frozen=True): + type: Literal["integer"] = "integer" + format: str = "int32" + maximum: int | None = None + exclusiveMaximum: int | bool | None = None + minimum: int | None = None + exclusiveMinimum: int | bool | None = None + multipleOf: int | None = None # TODO: implement support + const: int | None = None + enum: list[int] | None = None + nullable: bool = False + + @cached_property + def _max_int(self) -> int: + if self.format == "int64": + return 9223372036854775807 + return 2147483647 + + @cached_property + def _min_int(self) -> int: + if self.format == "int64": + return -9223372036854775808 + return -2147483648 + + @cached_property + def _max_value(self) -> int: + # OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum + # OAS 3.1: exclusiveMinimum/Maximum is an integer + if isinstance(self.exclusiveMaximum, int) and not isinstance( + self.exclusiveMaximum, bool + ): + return self.exclusiveMaximum - 1 + + if isinstance(self.maximum, int): + if self.exclusiveMaximum is True: + return self.maximum - 1 + return self.maximum + + return self._max_int + + @cached_property + def _min_value(self) -> int: + # OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum + # OAS 3.1: exclusiveMinimum/Maximum is an integer + if isinstance(self.exclusiveMinimum, int) and not isinstance( + self.exclusiveMinimum, bool + ): + return self.exclusiveMinimum + 1 + + if isinstance(self.minimum, int): + if self.exclusiveMinimum is True: + return self.minimum + 1 + return self.minimum + + return self._min_int + + def get_valid_value( + self, + operation_id: str | None = None, + ) -> tuple[int, IntegerSchema]: + """Generate a random int within the min/max range of the schema, if specified.""" + if self.const is not None: + return self.const, self + if self.enum is not None: + return choice(self.enum), self + + return randint(self._min_value, self._max_value), self + + def get_values_out_of_bounds(self, current_value: int) -> list[int]: # pylint: disable=unused-argument + invalid_values: list[int] = [] + + if self._min_value > self._min_int: + invalid_values.append(self._min_value - 1) + + if self._max_value < self._max_int: + invalid_values.append(self._max_value + 1) + + if invalid_values: + return invalid_values + + raise ValueError + + def get_invalid_value_from_const_or_enum(self) -> int: + valid_values = [] + if self.const is not None: + valid_values = [self.const] + if self.enum is not None: + valid_values = self.enum + + if not valid_values: + raise ValueError + + invalid_value = 0 + for value in valid_values: + invalid_value += abs(value) + abs(value) + + return invalid_value + + def get_invalid_value_from_constraint( + self, values_from_constraint: list[int] + ) -> int: + invalid_values = 2 * values_from_constraint + invalid_value = invalid_values.pop() + for value in invalid_values: + invalid_value = abs(invalid_value) + abs(value) + if not invalid_value: + invalid_value += 1 + return invalid_value + + @property + def can_be_invalidated(self) -> bool: + return True + + @property + def annotation_string(self) -> str: + return "int" + + @property + def python_type(self) -> builtins.type: + return int + + +class NumberSchema(SchemaBase[float], frozen=True): + type: Literal["number"] = "number" + maximum: int | float | None = None + exclusiveMaximum: int | float | bool | None = None + minimum: int | float | None = None + exclusiveMinimum: int | float | bool | None = None + multipleOf: int | None = None # TODO: implement support + const: int | float | None = None + enum: list[int | float] | None = None + nullable: bool = False + + @cached_property + def _max_float(self) -> float: + return 9223372036854775807.0 + + @cached_property + def _min_float(self) -> float: + return -9223372036854775808.0 + + @cached_property + def _max_value(self) -> float: + # OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum + # OAS 3.1: exclusiveMinimum/Maximum is an integer or a float + if isinstance(self.exclusiveMaximum, (int, float)) and not isinstance( + self.exclusiveMaximum, bool + ): + return self.exclusiveMaximum - 0.0000000001 + + if isinstance(self.maximum, (int, float)): + if self.exclusiveMaximum is True: + return self.maximum - 0.0000000001 + return self.maximum + + return self._max_float + + @cached_property + def _min_value(self) -> float: + # OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum + # OAS 3.1: exclusiveMinimum/Maximum is an integer or a float + if isinstance(self.exclusiveMinimum, (int, float)) and not isinstance( + self.exclusiveMinimum, bool + ): + return self.exclusiveMinimum + 0.0000000001 + + if isinstance(self.minimum, (int, float)): + if self.exclusiveMinimum is True: + return self.minimum + 0.0000000001 + return self.minimum + + return self._min_float + + def get_valid_value( + self, + operation_id: str | None = None, + ) -> tuple[float, NumberSchema]: + """Generate a random float within the min/max range of the schema, if specified.""" + if self.const is not None: + return self.const, self + if self.enum is not None: + return choice(self.enum), self + + return uniform(self._min_value, self._max_value), self + + def get_values_out_of_bounds(self, current_value: float) -> list[float]: # pylint: disable=unused-argument + invalid_values: list[float] = [] + + if self._min_value > self._min_float: + invalid_values.append(self._min_value - 0.000000001) + + if self._max_value < self._max_float: + invalid_values.append(self._max_value + 0.000000001) + + if invalid_values: + return invalid_values + + raise ValueError + + def get_invalid_value_from_const_or_enum(self) -> float: + valid_values = [] + if self.const is not None: + valid_values = [self.const] + if self.enum is not None: + valid_values = self.enum + + if not valid_values: + raise ValueError + + invalid_value = 0.0 + for value in valid_values: + invalid_value += abs(value) + abs(value) + + return invalid_value + + def get_invalid_value_from_constraint( + self, values_from_constraint: list[float] + ) -> float: + invalid_values = 2 * values_from_constraint + invalid_value = invalid_values.pop() + for value in invalid_values: + invalid_value = abs(invalid_value) + abs(value) + if not invalid_value: + invalid_value += 1 + return invalid_value + + @property + def can_be_invalidated(self) -> bool: + return True + + @property + def annotation_string(self) -> str: + return "float" + + @property + def python_type(self) -> builtins.type: + return float + + +class ArraySchema(SchemaBase[list[AI]], frozen=True): + type: Literal["array"] = "array" + items: SchemaObjectTypes + maxItems: int | None = None + minItems: int | None = None + uniqueItems: bool = False + const: list[AI] | None = None + enum: list[list[AI]] | None = None + nullable: bool = False + + def get_valid_value( + self, + operation_id: str | None = None, + ) -> tuple[list[AI], ArraySchema[AI]]: + if self.const is not None: + return self.const, self + + if self.enum is not None: + return choice(self.enum), self + + minimum = self.minItems if self.minItems is not None else 0 + maximum = self.maxItems if self.maxItems is not None else 1 + maximum = max(minimum, maximum) + + value: list[AI] = [] + number_of_items_to_generate = randint(minimum, maximum) + for _ in range(number_of_items_to_generate): + item_value = cast("AI", self.items.get_valid_value()[0]) + value.append(item_value) + return value, self + + def get_values_out_of_bounds(self, current_value: list[AI]) -> list[list[AI]]: + invalid_values: list[list[AI]] = [] + + if self.minItems: + invalid_value = current_value[0 : self.minItems - 1] + invalid_values.append(invalid_value) + + if self.maxItems is not None: + invalid_value = [] + if not current_value: + current_value = self.get_valid_value()[0] + + if not current_value: + current_value = [self.items.get_valid_value()[0]] # type: ignore[list-item] + + while len(invalid_value) <= self.maxItems: + invalid_value.append(choice(current_value)) + invalid_values.append(invalid_value) + + if invalid_values: + return invalid_values + + raise ValueError + + def get_invalid_value_from_const_or_enum(self) -> list[AI]: + valid_values = [] + if self.const is not None: + valid_values = [self.const] + if self.enum is not None: + valid_values = self.enum + + if not valid_values: + raise ValueError + + invalid_value = [] + for value in valid_values: + invalid_value.extend(value) + invalid_value.extend(value) + + return invalid_value + + def get_invalid_value_from_constraint( + self, values_from_constraint: list[list[AI]] + ) -> list[AI]: + values_from_constraint = deepcopy(values_from_constraint) + + valid_array = values_from_constraint.pop() + invalid_array: list[AI] = [] + for value in valid_array: + invalid_value = self.items.get_invalid_value_from_constraint( + values_from_constraint=[value], # type: ignore[list-item] + ) + invalid_array.append(invalid_value) # type: ignore[arg-type] + return invalid_array + + def get_invalid_data( + self, + valid_data: list[AI], + status_code: int, + invalid_property_default_code: int, + ) -> list[AI]: + """Return a data set with one of the properties set to an invalid value or type.""" + invalid_values: list[list[AI]] = [] + + relations = self.relations_mapping.get_body_relations_for_error_code( + error_code=status_code + ) + # TODO: handle relations applicable to arrays / lists + + if status_code == invalid_property_default_code: + try: + values_out_of_bounds = self.get_values_out_of_bounds( + current_value=valid_data + ) + invalid_values.extend(values_out_of_bounds) + except ValueError: + pass + try: + invalid_const_or_enum = self.get_invalid_value_from_const_or_enum() + invalid_values.append(invalid_const_or_enum) + except ValueError: + pass + if is_object_schema(self.items): + data_to_invalidate = deepcopy(valid_data) + valid_item = ( + data_to_invalidate.pop() + if valid_data + else self.items.get_valid_value()[0] + ) + invalid_item = self.items.get_invalid_data( + valid_data=valid_item, # type: ignore[arg-type] + status_code=status_code, + invalid_property_default_code=invalid_property_default_code, + ) + invalid_data = [*data_to_invalidate, invalid_item] + invalid_values.append(invalid_data) + + if not invalid_values: + raise ValueError( + f"No constraint can be broken to cause status_code {status_code}" + ) + return choice(invalid_values) + + @property + def can_be_invalidated(self) -> bool: + if ( + self.maxItems is not None + or self.minItems is not None + or self.uniqueItems + or self.const is not None + or self.enum is not None + ): + return True + if isinstance(self.items, (BooleanSchema, IntegerSchema, NumberSchema)): + return True + return False + + @property + def annotation_string(self) -> str: + return f"list[{self.items.annotation_string}]" + + @property + def python_type(self) -> builtins.type: + return list + + +# NOTE: Workaround for cyclic PropertiesMapping / SchemaObjectTypes annotations +def _get_properties_mapping_default() -> PropertiesMapping: + return _get_empty_properties_mapping() + + +class ObjectSchema(SchemaBase[dict[str, JSON]], frozen=True): + type: Literal["object"] = "object" + properties: PropertiesMapping = Field( + default_factory=_get_properties_mapping_default + ) + additionalProperties: SchemaObjectTypes | bool = True + required: list[str] = [] + maxProperties: int | None = None + minProperties: int | None = None + const: dict[str, JSON] | None = None + enum: list[dict[str, JSON]] | None = None + nullable: bool = False + + def get_valid_value( + self, + operation_id: str | None = None, + ) -> tuple[dict[str, JSON], ObjectSchema]: + if self.const is not None: + return self.const, self + + if self.enum is not None: + return choice(self.enum), self + + json_data: dict[str, Any] = {} + + property_names = self._get_property_names_to_process() + + for property_name in property_names: + property_schema = self.properties.root[property_name] + if property_schema.readOnly: + continue + + json_data[property_name] = self._get_data_for_property( + property_name=property_name, + property_schema=property_schema, + operation_id=operation_id, + ) + + return json_data, self + + def _get_property_names_to_process(self) -> list[str]: + property_names = [] + + properties = {} if self.properties is None else self.properties.root + for property_name in properties: + # register the oas_name + _ = get_safe_name_for_oas_name(property_name) + if constrained_values := self._get_constrained_values( + property_name=property_name + ): + # do not add properties that are configured to be ignored + if IGNORE in constrained_values: # type: ignore[comparison-overlap] + continue + property_names.append(property_name) + + max_properties = self.maxProperties + if max_properties and len(property_names) > max_properties: + required_properties = self.required + number_of_optional_properties = max_properties - len(required_properties) + optional_properties = [ + name for name in property_names if name not in required_properties + ] + selected_optional_properties = sample( + optional_properties, number_of_optional_properties + ) + property_names = required_properties + selected_optional_properties + + return property_names + + def _get_data_for_property( + self, + property_name: str, + property_schema: SchemaObjectTypes, + operation_id: str | None, + ) -> JSON: + if constrained_values := self._get_constrained_values( + property_name=property_name + ): + constrained_value = choice(constrained_values) + # Check if the chosen value is a nested relations_mapping; since a + # mapping is never instantiated, we can use isinstance(..., type) for this. + if isinstance(constrained_value, type): + property_schema.attach_relations_mapping(constrained_value) + valid_value, _ = property_schema.get_valid_value( + operation_id=operation_id + ) + return valid_value + + return constrained_value + + if ( + dependent_id := get_dependent_id( + relations_mapping=self.relations_mapping, + property_name=property_name, + operation_id=operation_id, + ) + ) is not None: + return dependent_id + + # Relations are mapped to endpoints; they are not attached to the property + # value schemas so update the schema before value generation + property_schema.attach_relations_mapping(self.relations_mapping) + return property_schema.get_valid_value(operation_id=operation_id)[0] + + def _get_constrained_values( + self, property_name: str + ) -> list[JSON | RelationsMappingType]: + relations = self.relations_mapping.get_relations() + values_list = [ + c.values + for c in relations + if ( + isinstance(c, PropertyValueConstraint) + and c.property_name == property_name + ) + ] + # values should be empty or contain 1 list of allowed values + return values_list.pop() if values_list else [] + + def get_values_out_of_bounds( + self, current_value: Mapping[str, JSON] + ) -> list[dict[str, JSON]]: + raise ValueError + + def get_invalid_value_from_const_or_enum(self) -> dict[str, JSON]: + valid_values = [] + if self.const is not None: + valid_values = [self.const] + if self.enum is not None: + valid_values = self.enum + + if not valid_values: + raise ValueError + + # This invalidation will not work for a const and may not work for + # an enum. In that case a different invalidation approach will be used. + invalid_value = {**valid_values[0]} + for value in valid_values: + for key in invalid_value.keys(): + invalid_value[key] = value.get(key) + if invalid_value not in valid_values: + return invalid_value + + raise ValueError + + def get_invalid_value_from_constraint( + self, values_from_constraint: list[dict[str, JSON]] + ) -> dict[str, JSON]: + values_from_constraint = deepcopy(values_from_constraint) + + valid_object = values_from_constraint.pop() + invalid_object: dict[str, JSON] = {} + for key, value in valid_object.items(): + python_type_of_value = type(value) + json_type_of_value = json_type_name_of_python_type(python_type_of_value) + schema = MediaTypeObject(schema={"type": json_type_of_value}).schema_ # pyright: ignore[reportArgumentType] + invalid_value = schema.get_invalid_value_from_constraint( # type: ignore[union-attr] + values_from_constraint=[value], # type: ignore[list-item] + ) + invalid_object[key] = invalid_value + return invalid_object + + def get_invalid_data( + self, + valid_data: dict[str, JSON], + status_code: int, + invalid_property_default_code: int, + ) -> dict[str, JSON]: + """Return a data set with one of the properties set to an invalid value or type.""" + properties: dict[str, JSON] = deepcopy(valid_data) + + relations = self.relations_mapping.get_body_relations_for_error_code( + error_code=status_code + ) + property_names = [r.property_name for r in relations] + + if status_code == invalid_property_default_code: + # add all properties defined in the schema, including optional properties + property_names.extend((self.properties.root.keys())) + if not property_names: + raise ValueError( + f"No property can be invalidated to cause status_code {status_code}" + ) + # Remove duplicates, then shuffle the property_names so different properties in + # the data dict are invalidated when rerunning the test. + shuffle(list(set(property_names))) + # The value of 1 property will be changed and since they are shuffled, take the first + property_name = property_names[0] + # if possible, invalidate a constraint but send otherwise valid data + id_dependencies = [ + r + for r in relations + if isinstance(r, IdDependency) and r.property_name == property_name + ] + if id_dependencies: + invalid_id = uuid4().hex + logger.debug( + f"Breaking IdDependency for status_code {status_code}: setting " + f"{property_name} to {invalid_id}" + ) + properties[property_name] = invalid_id + return properties + + invalid_value_from_constraint = [ + r.invalid_value + for r in relations + if isinstance(r, PropertyValueConstraint) + and r.property_name == property_name + and r.invalid_value_error_code == status_code + ] + if ( + invalid_value_from_constraint + and invalid_value_from_constraint[0] is not NOT_SET + ): + invalid_value = invalid_value_from_constraint[0] + if isinstance(invalid_value, Ignore): + properties.pop(property_name) + logger.debug( + f"Property {property_name} removed since the invalid_value " + f"was IGNORE (received from get_invalid_value)" + ) + else: + properties[property_name] = invalid_value + logger.debug( + f"Using invalid_value {invalid_value_from_constraint[0]} to " + f"invalidate property {property_name}" + ) + return properties + + value_schema = self.properties.root[property_name] + if isinstance(value_schema, UnionTypeSchema): + # Filter "type": "null" from the possible types since this indicates an + # optional / nullable property that can only be invalidated by sending + # invalid data of a non-null type + non_null_schemas = [ + s + for s in value_schema.resolved_schemas + if not isinstance(s, NullSchema) + ] + value_schema = choice(non_null_schemas) + + # there may not be a current_value when invalidating an optional property + current_value = properties.get(property_name, SENTINEL) + if current_value is SENTINEL: + current_value = value_schema.get_valid_value()[0] + + values_from_constraint = [ + r.values[0] + for r in relations + if isinstance(r, PropertyValueConstraint) + and r.property_name == property_name + ] + + invalid_value = value_schema.get_invalid_value( + valid_value=current_value, # type: ignore[arg-type] + values_from_constraint=values_from_constraint, + ) + if isinstance(invalid_value, Ignore): + properties.pop(property_name) + logger.debug( + f"Property {property_name} removed since the invalid_value " + f"was IGNORE (received from get_invalid_value)" + ) + else: + properties[property_name] = invalid_value + logger.debug( + f"Property {property_name} changed to {invalid_value} " + f"(received from get_invalid_value)" + ) + return properties + + def contains_properties(self, property_names: list[str]) -> bool: + if self.properties is None: + return False # pragma: no cover + for property_name in property_names: + if property_name not in self.properties.root: + return False + return True + + @property + def can_be_invalidated(self) -> bool: + if ( + self.required + or self.maxProperties is not None + or self.minProperties is not None + or self.const is not None + or self.enum is not None + ): + return True + return False + + @property + def annotation_string(self) -> str: + return "dict[str, JSON]" + + @property + def python_type(self) -> builtins.type: + return dict + + +ResolvedSchemaObjectTypes = Annotated[ + Union[ + ArraySchema, # type: ignore[type-arg] + BooleanSchema, + IntegerSchema, + NullSchema, + NumberSchema, + ObjectSchema, + StringSchema, + ], + Field(discriminator="type"), +] + +RESOLVED_SCHEMA_CLASS_TUPLE = ( + NullSchema, + BooleanSchema, + StringSchema, + IntegerSchema, + NumberSchema, + ArraySchema, + ObjectSchema, +) + + +class UnionTypeSchema(SchemaBase[JSON], frozen=True): + allOf: list["SchemaObjectTypes"] = [] + anyOf: list["SchemaObjectTypes"] = [] + oneOf: list["SchemaObjectTypes"] = [] + nullable: bool = False + + def get_valid_value( + self, + operation_id: str | None = None, + ) -> tuple[JSON, ResolvedSchemaObjectTypes]: + relations = ( + self.relations_mapping.get_relations() + + self.relations_mapping.get_parameter_relations() + ) + constrained_property_names = [relation.property_name for relation in relations] + + if not constrained_property_names: + resolved_schemas = self.resolved_schemas + chosen_schema = choice(resolved_schemas) + return chosen_schema.get_valid_value(operation_id=operation_id) + + valid_values = [] + valid_schemas = [] + for candidate in self.resolved_schemas: + if isinstance(candidate, ObjectSchema): + if candidate.contains_properties(constrained_property_names): + valid_schemas.append(candidate) + + if isinstance(candidate, UnionTypeSchema): + candidate.attach_relations_mapping(self.relations_mapping) + try: + valid_value = candidate.get_valid_value(operation_id=operation_id) + valid_values.append(valid_value) + except ValueError: + pass + for valid_schema in valid_schemas: + valid_value = valid_schema.get_valid_value(operation_id=operation_id) + valid_values.append(valid_value) + + if valid_values: + return choice(valid_values) + + # The constraints from the parent may not be applicable, resulting in no + # valid_values being generated. In that case, generated a random value as normal. + chosen_schema = choice(self.resolved_schemas) + return chosen_schema.get_valid_value(operation_id=operation_id) + + def get_values_out_of_bounds(self, current_value: JSON) -> list[JSON]: + raise ValueError + + @cached_property + def resolved_schemas(self) -> list[ResolvedSchemaObjectTypes]: + schemas_to_return: list[ResolvedSchemaObjectTypes] = [] + null_schema = None + + resolved_schemas = list(self._get_resolved_schemas()) + for schema in resolved_schemas: + # Prevent duplication of NullSchema when handling nullable models. + if isinstance(schema, NullSchema): + null_schema = schema + else: + schemas_to_return.append(schema) + if null_schema is not None: + schemas_to_return.append(null_schema) + return schemas_to_return + + def _get_resolved_schemas(self) -> Generator[ResolvedSchemaObjectTypes, None, None]: + if self.allOf: + properties_list: list[PropertiesMapping] = [] + additional_properties_list = [] + required_list = [] + max_properties_list = [] + min_properties_list = [] + nullable_list = [] + + schemas_to_process = [] + for schema in self.allOf: + if isinstance(schema, UnionTypeSchema): + schemas_to_process.extend(schema.resolved_schemas) + else: + schemas_to_process.append(schema) + + for schema in schemas_to_process: + if not isinstance(schema, ObjectSchema): + raise ValueError("allOf is only supported for ObjectSchemas") + + if schema.const is not None: + raise ValueError("allOf and models with a const are not compatible") + + if schema.enum: + raise ValueError("allOf and models with enums are not compatible") + + if schema.properties.root: + properties_list.append(schema.properties) + additional_properties_list.append(schema.additionalProperties) + required_list += schema.required + max_properties_list.append(schema.maxProperties) + min_properties_list.append(schema.minProperties) + nullable_list.append(schema.nullable) + + properties_dicts = [mapping.root for mapping in properties_list] + merged_properties = dict(ChainMap(*properties_dicts)) + + if True in additional_properties_list: + additional_properties_value: bool | SchemaObjectTypes = True + else: + additional_properties_types = [] + for additional_properties_item in additional_properties_list: + if isinstance( + additional_properties_item, RESOLVED_SCHEMA_CLASS_TUPLE + ): + additional_properties_types.append(additional_properties_item) + if isinstance(additional_properties_item, UnionTypeSchema): + additional_properties_types.extend( + additional_properties_item.resolved_schemas + ) + if not additional_properties_types: + additional_properties_value = False + else: + additional_properties_value = UnionTypeSchema( + anyOf=additional_properties_types, + ) + + max_properties = [max for max in max_properties_list if max is not None] + min_properties = [min for min in min_properties_list if min is not None] + max_propeties_value = max(max_properties) if max_properties else None + min_propeties_value = min(min_properties) if min_properties else None + + merged_schema = ObjectSchema( + type="object", + properties=PropertiesMapping(root=merged_properties), + additionalProperties=additional_properties_value, + required=required_list, + maxProperties=max_propeties_value, + minProperties=min_propeties_value, + nullable=False, + ) + merged_schema.attach_relations_mapping(self.relations_mapping) + yield merged_schema + # If all schemas are nullable the merged schema is treated as nullable. + if all(nullable_list): + null_schema = NullSchema() + null_schema.attach_relations_mapping(self.relations_mapping) + yield null_schema + else: + for schema in self.anyOf + self.oneOf: + if isinstance(schema, RESOLVED_SCHEMA_CLASS_TUPLE): + if schema.nullable: + schema.__dict__["nullable"] = False + null_schema = NullSchema() + null_schema.attach_relations_mapping(self.relations_mapping) + yield null_schema + yield schema + else: + yield from schema.resolved_schemas + + def get_invalid_value_from_const_or_enum(self) -> JSON: + raise ValueError + + def get_invalid_value_from_constraint( + self, values_from_constraint: list[JSON] + ) -> JSON: + raise ValueError + + @property + def annotation_string(self) -> str: + unique_annotations = {s.annotation_string for s in self.resolved_schemas} + return " | ".join(unique_annotations) + + +SchemaObjectTypes: TypeAlias = ResolvedSchemaObjectTypes | UnionTypeSchema + + +class PropertiesMapping(RootModel[dict[str, SchemaObjectTypes]], frozen=True): ... + + +def _get_empty_properties_mapping() -> PropertiesMapping: + return PropertiesMapping(root={}) + + +class ParameterObject(BaseModel): + name: str + in_: str = Field(..., alias="in") + required: bool = False + description: str = "" + schema_: SchemaObjectTypes | None = Field(None, alias="schema") + relations_mapping: RelationsMappingType | None = None + + def attach_relations_mapping(self, relations_mapping: RelationsMappingType) -> None: + if self.schema_: # pragma: no branch + self.schema_.attach_relations_mapping(relations_mapping) + + def replace_nullable_with_union(self) -> None: + if self.schema_: # pragma: no branch + processed_schema = nullable_schema_to_union_schema(self.schema_) + self.schema_ = processed_schema + + +class MediaTypeObject(BaseModel): + schema_: SchemaObjectTypes | None = Field(None, alias="schema") + + +class RequestBodyObject(BaseModel): + content: dict[str, MediaTypeObject] + required: bool = False + description: str = "" + + @cached_property + def schema_(self) -> SchemaObjectTypes | None: + if not self.mime_type: + return None + + if len(self._json_schemas) > 1: + logger.info( + f"Multiple JSON media types defined for requestBody, " + f"using the first candidate from {self.content}" + ) + return self._json_schemas[self.mime_type] + + @cached_property + def mime_type(self) -> str | None: + if not self._json_schemas: + return None + + return next(iter(self._json_schemas)) + + @cached_property + def _json_schemas(self) -> dict[str, SchemaObjectTypes]: + json_schemas = { + mime_type: media_type.schema_ + for mime_type, media_type in self.content.items() + if "json" in mime_type and media_type.schema_ is not None + } + return json_schemas + + def attach_relations_mapping(self, relations_mapping: RelationsMappingType) -> None: + for media_object_type in self.content.values(): + if media_object_type and media_object_type.schema_: # pragma: no branch + media_object_type.schema_.attach_relations_mapping(relations_mapping) + + def replace_nullable_with_union(self) -> None: + for media_object_type in self.content.values(): + if media_object_type and media_object_type.schema_: # pragma: no branch + processed_schema = nullable_schema_to_union_schema( + media_object_type.schema_ + ) + media_object_type.schema_ = processed_schema + + +class HeaderObject(BaseModel): ... + + +class LinkObject(BaseModel): ... + + +class ResponseObject(BaseModel): + description: str + content: dict[str, MediaTypeObject] = {} + headers: dict[str, HeaderObject] = {} + links: dict[str, LinkObject] = {} + + +class OperationObject(BaseModel): + operationId: str | None = None + summary: str = "" + description: str = "" + tags: list[str] = [] + parameters: list[ParameterObject] = [] + requestBody: RequestBodyObject | None = None + responses: dict[str, ResponseObject] = {} + relations_mapping: RelationsMappingType | None = None + + def update_parameters(self, parameters: list[ParameterObject]) -> None: + self.parameters.extend(parameters) + + def attach_relations_mappings(self) -> None: + if not self.relations_mapping: + return + + if self.requestBody: + self.requestBody.attach_relations_mapping(self.relations_mapping) + + for parameter_object in self.parameters: + parameter_object.attach_relations_mapping(self.relations_mapping) + + def replace_nullable_with_union(self) -> None: + if self.requestBody: + self.requestBody.replace_nullable_with_union() + + for parameter_object in self.parameters: + parameter_object.replace_nullable_with_union() + + +class PathItemObject(BaseModel): + get: OperationObject | None = None + post: OperationObject | None = None + patch: OperationObject | None = None + put: OperationObject | None = None + delete: OperationObject | None = None + summary: str = "" + description: str = "" + parameters: list[ParameterObject] = [] + relations_mapping: RelationsMappingType | None = None + id_mapper: tuple[str, Callable[[str], str]] = ( + "id", + dummy_transformer, + ) + + @property + def operations(self) -> dict[str, OperationObject]: + return { + k: v for k, v in self.__dict__.items() if isinstance(v, OperationObject) + } + + def update_operation_parameters(self) -> None: + if not self.parameters: + return + + operations_to_update = self.operations + for operation_object in operations_to_update.values(): + operation_object.update_parameters(self.parameters) + + def attach_relations_mappings(self) -> None: + for operation_object in self.operations.values(): + operation_object.attach_relations_mappings() + + def replace_nullable_with_union(self) -> None: + for operation_object in self.operations.values(): + operation_object.attach_relations_mappings() + operation_object.replace_nullable_with_union() + + +class InfoObject(BaseModel): + title: str + version: str + summary: str = "" + description: str = "" + + +class OpenApiObject(BaseModel): + info: InfoObject + paths: dict[str, PathItemObject] + + +def nullable_schema_to_union_schema(schema: SchemaObjectTypes) -> SchemaObjectTypes: + if not schema.nullable: + return schema + + schema.__dict__["nullable"] = False + null_schema = NullSchema() + null_schema.attach_relations_mapping(schema.relations_mapping) + union_schema = UnionTypeSchema(oneOf=[schema, null_schema]) + union_schema.attach_relations_mapping(schema.relations_mapping) + return union_schema + + +# TODO: move to keyword_logic? +def get_dependent_id( + relations_mapping: RelationsMappingType | None, + property_name: str, + operation_id: str | None, +) -> str | int | float | None: + relations = relations_mapping.get_relations() if relations_mapping else [] + # multiple get paths are possible based on the operation being performed + id_get_paths = [ + (d.get_path, d.operation_id) + for d in relations + if (isinstance(d, IdDependency) and d.property_name == property_name) + ] + if not id_get_paths: + return None + if len(id_get_paths) == 1: + id_get_path, _ = id_get_paths.pop() + else: + try: + [id_get_path] = [ + path for path, operation in id_get_paths if operation == operation_id + ] + # There could be multiple get_paths, but not one for the current operation + except ValueError: + return None + + valid_id = cast( + str | int | float, + run_keyword("get_valid_id_for_path", id_get_path), # pyright: ignore[reportArgumentType] + ) + logger.debug(f"get_dependent_id for {id_get_path} returned {valid_id}") + return valid_id diff --git a/src/OpenApiLibCore/request_data.py b/src/OpenApiLibCore/models/request_data.py similarity index 78% rename from src/OpenApiLibCore/request_data.py rename to src/OpenApiLibCore/models/request_data.py index 707539d..996dd30 100644 --- a/src/OpenApiLibCore/request_data.py +++ b/src/OpenApiLibCore/models/request_data.py @@ -4,17 +4,15 @@ from dataclasses import dataclass, field from functools import cached_property from random import sample -from typing import Any from OpenApiLibCore.annotations import JSON -from OpenApiLibCore.dto_base import Dto -from OpenApiLibCore.dto_utils import DefaultDto -from OpenApiLibCore.models import ( +from OpenApiLibCore.models.oas_models import ( ObjectSchema, ParameterObject, ResolvedSchemaObjectTypes, UnionTypeSchema, ) +from OpenApiLibCore.protocols import RelationsMappingType @dataclass @@ -24,14 +22,15 @@ class RequestValues: url: str method: str params: dict[str, JSON] = field(default_factory=dict) - headers: dict[str, JSON] = field(default_factory=dict) - json_data: dict[str, JSON] = field(default_factory=dict) + headers: dict[str, str] = field(default_factory=dict) + json_data: JSON = None def override_body_value(self, name: str, value: JSON) -> None: - if name in self.json_data: + # TODO: add support for overriding list body items + if isinstance(self.json_data, dict) and name in self.json_data: self.json_data[name] = value - def override_header_value(self, name: str, value: JSON) -> None: + def override_header_value(self, name: str, value: str) -> None: if name in self.headers: self.headers[name] = value @@ -41,25 +40,27 @@ def override_param_value(self, name: str, value: JSON) -> None: def override_request_value(self, name: str, value: JSON) -> None: self.override_body_value(name=name, value=value) - self.override_header_value(name=name, value=value) + self.override_header_value(name=name, value=str(value)) self.override_param_value(name=name, value=value) def remove_parameters(self, parameters: list[str]) -> None: for parameter in parameters: _ = self.params.pop(parameter, None) _ = self.headers.pop(parameter, None) - _ = self.json_data.pop(parameter, None) + if isinstance(self.json_data, dict): + _ = self.json_data.pop(parameter, None) @dataclass class RequestData: """Helper class to manage parameters used when making requests.""" - dto: Dto | DefaultDto = field(default_factory=DefaultDto) - body_schema: ObjectSchema | None = None + valid_data: JSON + relations_mapping: RelationsMappingType + body_schema: ResolvedSchemaObjectTypes | None = None parameters: list[ParameterObject] = field(default_factory=list) params: dict[str, JSON] = field(default_factory=dict) - headers: dict[str, JSON] = field(default_factory=dict) + headers: dict[str, str] = field(default_factory=dict) has_body: bool = True def __post_init__(self) -> None: @@ -69,17 +70,20 @@ def __post_init__(self) -> None: @property def has_optional_properties(self) -> bool: - """Whether or not the dto data (json data) contains optional properties.""" + """Whether or not the json data contains optional properties.""" def is_required_property(property_name: str) -> bool: return property_name in self.required_property_names - properties = (self.dto.as_dict()).keys() + if not isinstance(self.valid_data, dict): + return False + + properties = (self.valid_data).keys() return not all(map(is_required_property, properties)) @property def required_property_names(self) -> list[str]: - if self.body_schema: + if isinstance(self.body_schema, ObjectSchema): return self.body_schema.required return [] @@ -165,28 +169,38 @@ def headers_that_can_be_invalidated(self) -> set[str]: return result - def get_required_properties_dict(self) -> dict[str, Any]: - """Get the json-compatible dto data containing only the required properties.""" - relations = self.dto.get_relations() + def get_required_properties_dict(self) -> dict[str, JSON]: + """Get the json data containing only the required properties.""" + relations = self.relations_mapping.get_relations() mandatory_properties = [ relation.property_name for relation in relations if getattr(relation, "treat_as_mandatory", False) ] - required_properties = self.body_schema.required if self.body_schema else [] + required_properties = ( + self.body_schema.required + if isinstance(self.body_schema, ObjectSchema) + else [] + ) required_properties.extend(mandatory_properties) - required_properties_dict: dict[str, Any] = {} - for key, value in (self.dto.as_dict()).items(): + required_properties_dict: dict[str, JSON] = {} + if not isinstance(self.valid_data, dict): + return required_properties_dict + + for key, value in self.valid_data.items(): if key in required_properties: required_properties_dict[key] = value return required_properties_dict - def get_minimal_body_dict(self) -> dict[str, Any]: + def get_minimal_body_dict(self) -> dict[str, JSON]: required_properties_dict = self.get_required_properties_dict() min_properties = 0 - if self.body_schema and self.body_schema.minProperties is not None: + if ( + isinstance(self.body_schema, ObjectSchema) + and self.body_schema.minProperties is not None + ): min_properties = self.body_schema.minProperties number_of_optional_properties_to_add = min_properties - len( @@ -196,9 +210,12 @@ def get_minimal_body_dict(self) -> dict[str, Any]: if number_of_optional_properties_to_add < 1: return required_properties_dict + if not isinstance(self.valid_data, dict): + return required_properties_dict + optional_properties_dict = { k: v - for k, v in self.dto.as_dict().items() + for k, v in self.valid_data.items() if k not in required_properties_dict } optional_properties_to_keep = sample( @@ -218,7 +235,7 @@ def get_required_params(self) -> dict[str, JSON]: k: v for k, v in self.params.items() if k in self.required_parameter_names } - def get_required_headers(self) -> dict[str, JSON]: + def get_required_headers(self) -> dict[str, str]: """Get the headers dict containing only the required headers.""" return { k: v for k, v in self.headers.items() if k in self.required_parameter_names @@ -230,7 +247,7 @@ def required_parameter_names(self) -> list[str]: The names of the mandatory parameters, including the parameters configured to be treated as mandatory using a PropertyValueConstraint. """ - relations = self.dto.get_parameter_relations() + relations = self.relations_mapping.get_parameter_relations() mandatory_property_names = [ relation.property_name for relation in relations diff --git a/src/OpenApiLibCore/models/resource_relations.py b/src/OpenApiLibCore/models/resource_relations.py new file mode 100644 index 0000000..9f38d36 --- /dev/null +++ b/src/OpenApiLibCore/models/resource_relations.py @@ -0,0 +1,63 @@ +from abc import ABC +from dataclasses import dataclass +from typing import Any + +NOT_SET = object() + + +class ResourceRelation(ABC): + """ABC for all resource relations or restrictions within the API.""" + + property_name: str + error_code: int + + +@dataclass +class PathPropertiesConstraint(ResourceRelation): + """The value to be used as the ``path`` for related requests.""" + + path: str + property_name: str = "id" + invalid_value: Any = NOT_SET + invalid_value_error_code: int = 422 + error_code: int = 404 + + +@dataclass +class PropertyValueConstraint(ResourceRelation): + """The allowed values for property_name.""" + + property_name: str + values: list[Any] + invalid_value: Any = NOT_SET + invalid_value_error_code: int = 422 + error_code: int = 422 + treat_as_mandatory: bool = False + + +@dataclass +class IdDependency(ResourceRelation): + """The path where a valid id for the property_name can be gotten (using GET).""" + + property_name: str + get_path: str + operation_id: str = "" + error_code: int = 422 + + +@dataclass +class IdReference(ResourceRelation): + """The path where a resource that needs this resource's id can be created (using POST).""" + + property_name: str + post_path: str + error_code: int = 422 + + +@dataclass +class UniquePropertyValueConstraint(ResourceRelation): + """The value of the property must be unique within the resource scope.""" + + property_name: str + value: Any + error_code: int = 422 diff --git a/src/OpenApiLibCore/oas_cache.py b/src/OpenApiLibCore/oas_cache.py deleted file mode 100644 index a9c4d24..0000000 --- a/src/OpenApiLibCore/oas_cache.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Module holding the (global) parser cache.""" - -from dataclasses import dataclass - -from openapi_core import Spec -from prance import ResolvingParser - -from OpenApiLibCore.protocols import ResponseValidatorType - - -@dataclass -class CachedParser: - parser: ResolvingParser - validation_spec: Spec - response_validator: ResponseValidatorType - - -PARSER_CACHE: dict[str, CachedParser] = {} diff --git a/src/OpenApiLibCore/openapi_libcore.libspec b/src/OpenApiLibCore/openapi_libcore.libspec index bf710d2..395c830 100644 --- a/src/OpenApiLibCore/openapi_libcore.libspec +++ b/src/OpenApiLibCore/openapi_libcore.libspec @@ -1,14 +1,14 @@ - -1.0.5 + +2.0.0b1 The OpenApiLibCore library provides the keywords and core logic to interact with an OpenAPI server. Visit the <a href="./index.html" target="_blank">OpenApiTools documentation</a> for an introduction. - - + + source @@ -41,8 +41,8 @@ Visit the <a href="./index.html" target="_blank">OpenApiTools documentatio - -invalid_property_default_response + +invalid_data_default_response 422 @@ -120,7 +120,7 @@ Visit the <a href="./index.html" target="_blank">OpenApiTools documentatio extra_headers - + @@ -129,11 +129,11 @@ Visit the <a href="./index.html" target="_blank">OpenApiTools documentatio cookies - + - + None @@ -141,7 +141,7 @@ Visit the <a href="./index.html" target="_blank">OpenApiTools documentatio proxies - + @@ -189,7 +189,7 @@ by Response validation. <h3>mappings_path</h3> See the Advanced Use tab for an in-depth explanation. -<h3>invalid_property_default_response</h3> +<h3>invalid_data_default_response</h3> The default response code for requests with a JSON body that does not comply with the schema. Example: a value outside the specified range or a string value @@ -272,25 +272,22 @@ A dictionary of <code>"protocol": "proxy url"</code> to use for all - - + + href - + referenced_resource - - - Attempt to GET the resource referenced by the <span class="name">href</span> and validate it's equal -to the provided <span class="name">referenced_resource</span> object / dictionary. -Attempt to GET the resource referenced by the `href` and validate it's equal to the provided `referenced_resource` object / dictionary. +to the provided <span class="name">referenced_resource</span>. +Attempt to GET the resource referenced by the `href` and validate it's equal to the provided `referenced_resource`. - + url @@ -348,18 +345,28 @@ to the provided <span class="name">referenced_resource</span> object -Perform a request using the security token or authentication set in the library. - -<span class="name">json_data</span>, <span class="name">data</span> and <span class="name">files</span> are passed to <span class="name">requests.request</span>s <span class="name">json</span>, -<span class="name">data</span> and <span class="name">files</span> parameters unaltered. -See the requests documentation for details: -https://requests.readthedocs.io/en/latest/api/#requests.request +See the <a href="#Perform%20Authorized%20Request" class="name">Perform Authorized Request</a> keyword. -> Note: provided username / password or auth objects take precedence over token - based security -Perform a request using the security token or authentication set in the library. +The difference between these keywords is that this keyword accepts separate +arguments where <a href="#Perform%20Validated%20Request" class="name">Perform Validated Request</a> accepts a <span class="name">RequestValues</span> object +(see the <a href="#Get%20Request%20Values" class="name">Get Request Values</a> keyword for details). +See the `Perform Authorized Request` keyword. - + + + +request_values + + + + + + + +Convert a RequestValues object to a dictionary. +Convert a RequestValues object to a dictionary. + + url @@ -374,7 +381,7 @@ https://requests.readthedocs.io/en/latest/api/#requests.request is used by the resource defined by the <span class="name">resource_relation</span>. Ensure that the (right-most) `id` of the resource referenced by the `url` is used by the resource defined by the `resource_relation`. - + url @@ -388,7 +395,7 @@ is used by the resource defined by the <span class="name">resource_relatio <span class="name">ids</span> from the response. Perform a GET request on the `url` and return the list of resource `ids` from the response. - + url @@ -407,18 +414,15 @@ is used by the resource defined by the <span class="name">resource_relatio - - - - -Return <span class="name">json_data</span> based on the <span class="name">dto</span> on the <span class="name">request_data</span> that will cause -the provided <span class="name">status_code</span> for the <span class="name">method</span> operation on the <span class="name">url</span>. + +Return <span class="name">json_data</span> based on the <span class="name">relations_mapping</span> on the <span class="name">request_data</span> that +will cause the provided <span class="name">status_code</span> for the <span class="name">method</span> operation on the <span class="name">url</span>. > Note: applicable UniquePropertyValueConstraint and IdReference Relations are considered before changes to <span class="name">json_data</span> are made. -Return `json_data` based on the `dto` on the `request_data` that will cause the provided `status_code` for the `method` operation on the `url`. +Return `json_data` based on the `relations_mapping` on the `request_data` that will cause the provided `status_code` for the `method` operation on the `url`. - + status_code @@ -436,24 +440,19 @@ the provided <span class="name">status_code</span> for the <span - + Returns a version of <span class="name">params, headers</span> as present on <span class="name">request_data</span> that has been modified to cause the provided <span class="name">status_code</span>. Returns a version of `params, headers` as present on `request_data` that has been modified to cause the provided `status_code`. - - + + valid_url - -path - - - expected_status_code @@ -463,14 +462,14 @@ been modified to cause the provided <span class="name">status_code</spa Return an url with all the path parameters in the <span class="name">valid_url</span> replaced by a random UUID if no PathPropertiesConstraint is mapped for the <span class="name">"get"</span> operation -on the mapped <a href="#type-Path" class="name">path</a> and <span class="name">expected_status_code</span>. +on the related <a href="#type-Path" class="name">path</a> and <span class="name">expected_status_code</span>. If a PathPropertiesConstraint is mapped, the <span class="name">invalid_value</span> is returned. Raises: ValueError if the valid_url cannot be invalidated. -Return an url with all the path parameters in the `valid_url` replaced by a random UUID if no PathPropertiesConstraint is mapped for the `"get"` operation on the mapped `path` and `expected_status_code`. If a PathPropertiesConstraint is mapped, the `invalid_value` is returned. +Return an url with all the path parameters in the `valid_url` replaced by a random UUID if no PathPropertiesConstraint is mapped for the `"get"` operation on the related `path` and `expected_status_code`. If a PathPropertiesConstraint is mapped, the `invalid_value` is returned. - - + + url @@ -479,9 +478,18 @@ Raises: ValueError if the valid_url cannot be invalidated. method - -dto - + +json_data + + + + + + +relations_mapping + + + conflict_status_code @@ -493,11 +501,11 @@ Raises: ValueError if the valid_url cannot be invalidated. Return <span class="name">json_data</span> based on the <span class="name">UniquePropertyValueConstraint</span> that must be -returned by the <span class="name">get_relations</span> implementation on the <span class="name">dto</span> for the given -<span class="name">conflict_status_code</span>. -Return `json_data` based on the `UniquePropertyValueConstraint` that must be returned by the `get_relations` implementation on the `dto` for the given `conflict_status_code`. +returned by the <span class="name">get_relations</span> implementation on the <span class="name">relations_mapping</span> for +the given <span class="name">conflict_status_code</span>. +Return `json_data` based on the `UniquePropertyValueConstraint` that must be returned by the `get_relations` implementation on the `relations_mapping` for the given `conflict_status_code`. - + url @@ -508,7 +516,7 @@ returned by the <span class="name">get_relations</span> implementati Return the path as found in the <span class="name">paths</span> section based on the given <span class="name">url</span>. Return the path as found in the `paths` section based on the given `url`. - + path @@ -523,7 +531,7 @@ returned by the <span class="name">get_relations</span> implementati Return an object with valid request data for body, headers and query params. Return an object with valid request data for body, headers and query params. - + path @@ -535,7 +543,7 @@ returned by the <span class="name">get_relations</span> implementati overrides - + @@ -543,10 +551,58 @@ returned by the <span class="name">get_relations</span> implementati -Return an object with all (valid) request values needed to make a request. +Return an object with all (valid) request values needed to make a request. + +The <span class="name">overrides</span> dictionary can be used to pass specific values for parameters +instead of having them be generated automatically. Return an object with all (valid) request values needed to make a request. - + + + +url + + + +method + + + +params + + + + +{} + + +headers + + + + +{} + + +json_data + + + + +None + + + +This keyword can be used to instantiate a RequestValues object that can be used +with the <a href="#Perform%20Authorized%20Request" class="name">Perform Authorized Request</a> and <a href="#Perform%20Validated%20Request" class="name">Perform Validated Request</a> keywords. + +This is a utility keyword that can make certain test cases or keywords more +concise, but logging and debugging information will be more limited. + +See also the <a href="#Get%20Request%20Values" class="name">Get Request Values</a> keyword. +This keyword can be used to instantiate a RequestValues object that can be used with the `Perform Authorized Request` and `Perform Validated Request` keywords. + + path @@ -564,7 +620,7 @@ To prevent resource conflicts with other test cases, a new resource is created (by a POST operation) if possible. Support keyword that returns the `id` for an existing resource at `path`. - + path @@ -582,7 +638,44 @@ keyword will be executed to retrieve valid ids for the path parameters. [https://marketsquare.github.io/robotframework-openapitools/advanced_use.html | here]. This keyword returns a valid url for the given `path`. - + + + +request_values + + + +data + + + + +None + + +files + + + + +None + + + +Perform a request using the security token or authentication set in the library. + +<span class="name">json_data</span>, <span class="name">data</span> and <span class="name">files</span> are passed to <span class="name">requests.request</span>s <span class="name">json</span>, +<span class="name">data</span> and <span class="name">files</span> parameters unaltered. +See the requests documentation for details: +https://requests.readthedocs.io/en/latest/api/#requests.request + +See also <a href="#Authorized%20Request" class="name">Authorized Request</a> and <a href="#Get%20Request%20Values" class="name">Get Request Values</a>. + +> Note: provided username / password or auth objects take precedence over token + based security +Perform a request using the security token or authentication set in the library. + + path @@ -598,7 +691,7 @@ keyword will be executed to retrieve valid ids for the path parameters. original_data - + @@ -607,10 +700,12 @@ keyword will be executed to retrieve valid ids for the path parameters. This keyword first calls the Authorized Request keyword, then the Validate Response keyword and finally validates, for <span class="name">DELETE</span> operations, whether -the target resource was indeed deleted (OK response) or not (error responses). +the target resource was indeed deleted (OK response) or not (error responses). + +See also <a href="#Validated%20Request" class="name">Validated Request</a> and <a href="#Get%20Request%20Values" class="name">Get Request Values</a>. This keyword first calls the Authorized Request keyword, then the Validate Response keyword and finally validates, for `DELETE` operations, whether the target resource was indeed deleted (OK response) or not (error responses). - + auth @@ -623,7 +718,7 @@ After calling this keyword, subsequent requests will use the provided <span class="name">auth</span> instance. Set the `auth` used for authentication after the library is imported. - + username @@ -641,7 +736,7 @@ After calling this keyword, subsequent requests will use the provided credentials. Set the `username` and `password` used for basic authentication after the library is imported. - + extra_headers @@ -657,7 +752,7 @@ After calling this keyword, subsequent requests will use the provided <span class="name">extra_headers</span>. Set the `extra_headers` used in requests after the library is imported. - + origin @@ -674,7 +769,7 @@ In combination with OpenApiLibCore, the <span class="name">origin</span target another server that hosts an API that complies to the same OAS. Set the `origin` after the library is imported. - + security_token @@ -686,8 +781,8 @@ target another server that hosts an API that complies to the same OAS. After calling this keyword, subsequent requests will use the provided token. Set the `security_token` after the library is imported. - - + + path @@ -696,12 +791,9 @@ After calling this keyword, subsequent requests will use the provided token.response - + original_data - - - {} @@ -715,18 +807,18 @@ After calling this keyword, subsequent requests will use the provided token. Validate the `response` by performing the following validations: - validate the `response` against the openapi schema for the `path` - validate that the response does not contain extra properties - validate that a href, if present, refers to the correct resource - validate that the value for a property that is in the response is equal to the property value that was send - validate that no `original_data` is preserved when performing a PUT operation - validate that a PATCH operation only updates the provided properties - + response -Validate the <span class="name">response</span> against the OpenAPI Spec that is +Validate the <span class="name">response</span> against the OpenAPI spec that is loaded during library initialization. -Validate the `response` against the OpenAPI Spec that is loaded during library initialization. +Validate the `response` against the OpenAPI spec that is loaded during library initialization. - + response @@ -734,7 +826,7 @@ loaded during library initialization. original_data - + @@ -747,6 +839,64 @@ In case a PATCH request, validate that only the properties that were patched have changed and that other properties are still at their pre-patch values. Validate that each property that was send that is in the response has the value that was send. In case a PATCH request, validate that only the properties that were patched have changed and that other properties are still at their pre-patch values. + + + +path + + + +status_code + + + +url + + + +method + + + +params + + + + +{} + + +headers + + + + +{} + + +json_data + + + + +None + + +original_data + + + + +{} + + +See the <a href="#Perform%20Validated%20Request" class="name">Perform Validated Request</a> keyword. + +The difference between these keywords is that this keyword accepts separate +arguments where <a href="#Perform%20Validated%20Request" class="name">Perform Validated Request</a> accepts a <span class="name">RequestValues</span> object +(see the <a href="#Get%20Request%20Values" class="name">Get Request Values</a> keyword for details). +See the `Perform Validated Request` keyword. + @@ -756,10 +906,11 @@ have changed and that other properties are still at their pre-patch values. Authorized Request +Perform Authorized Request -<p>Strings <code>TRUE</code>, <code>YES</code>, <code>ON</code> and <code>1</code> are converted to Boolean <code>True</code>, the empty string as well as strings <code>FALSE</code>, <code>NO</code>, <code>OFF</code> and <code>0</code> are converted to Boolean <code>False</code>, and the string <code>NONE</code> is converted to the Python <code>None</code> object. Other strings and other accepted values are passed as-is, allowing keywords to handle them specially if needed. All string comparisons are case-insensitive.</p> +<p>Strings <code>TRUE</code>, <code>YES</code>, <code>ON</code>, <code>1</code> and possible localization specific "true strings" are converted to Boolean <code>True</code>, the empty string, strings <code>FALSE</code>, <code>NO</code>, <code>OFF</code> and <code>0</code> and possibly localization specific "false strings" are converted to Boolean <code>False</code>, and the string <code>NONE</code> is converted to the Python <code>None</code> object. Other strings and all other values are passed as-is, allowing keywords to handle them specially if needed. All string comparisons are case-insensitive.</p> <p>Examples: <code>TRUE</code> (converted to <code>True</code>), <code>off</code> (converted to <code>False</code>), <code>example</code> (used as-is)</p> string @@ -772,7 +923,8 @@ have changed and that other properties are still at their pre-patch values. -<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#dict">dictionary</a> literals. They are converted to actual dictionaries using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function. They can contain any values <code>ast.literal_eval</code> supports, including dictionaries and other containers.</p> +<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#dict">dictionary</a> literals. They are converted to actual dictionaries using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function. They can contain any values <code>ast.literal_eval</code> supports, including dictionaries and other collections.</p> +<p>Any mapping is accepted and converted to a <code>dict</code>.</p> <p>If the type has nested types like <code>dict[str, int]</code>, items are converted to those types automatically. This in new in Robot Framework 6.0.</p> <p>Examples: <code>{'a': 1, 'b': 2}</code>, <code>{'key': 1, 'nested': {'key': 2}}</code></p> @@ -780,22 +932,18 @@ have changed and that other properties are still at their pre-patch values.Mapping -__init__ -Assert Href To Resource Is Valid Authorized Request -Get Invalid Body Data +Convert Request Values To Dict Get Invalidated Parameters Get Json Data With Conflict -Get Request Values -Perform Validated Request +Get Request Values Object Set Extra Headers -Validate Response -Validate Send Response +Validated Request <p>Conversion is done using Python's <a href="https://docs.python.org/library/functions.html#float">float</a> built-in function.</p> -<p>Starting from RF 4.1, spaces and underscores can be used as visual separators for digit grouping purposes.</p> +<p>Spaces and underscores can be used as visual separators for digit grouping purposes.</p> <p>Examples: <code>3.14</code>, <code>2.9979e8</code>, <code>10 000.000 01</code></p> string @@ -807,8 +955,7 @@ have changed and that other properties are still at their pre-patch values. <p>Conversion is done using Python's <a href="https://docs.python.org/library/functions.html#int">int</a> built-in function. Floating point numbers are accepted only if they can be represented as integers exactly. For example, <code>1.0</code> is accepted and <code>1.1</code> is not.</p> -<p>Starting from RF 4.1, it is possible to use hexadecimal, octal and binary numbers by prefixing values with <code>0x</code>, <code>0o</code> and <code>0b</code>, respectively.</p> -<p>Starting from RF 4.1, spaces and underscores can be used as visual separators for digit grouping purposes.</p> +<p>It is possible to use hexadecimal, octal and binary numbers by prefixing values with <code>0x</code>, <code>0o</code> and <code>0b</code>, respectively. Spaces and underscores can be used as visual separators for digit grouping purposes.</p> <p>Examples: <code>42</code>, <code>-1</code>, <code>0b1010</code>, <code>10 000 000</code>, <code>0xBAD_C0FFEE</code></p> string @@ -822,12 +969,15 @@ have changed and that other properties are still at their pre-patch values.Get Json Data With Conflict Get Valid Id For Path Perform Validated Request +Validated Request -<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#list">list</a> literals. They are converted to actual lists using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function. They can contain any values <code>ast.literal_eval</code> supports, including lists and other containers.</p> -<p>If the type has nested types like <code>list[int]</code>, items are converted to those types automatically. This in new in Robot Framework 6.0.</p> -<p>Examples: <code>['one', 'two']</code>, <code>[('one', 1), ('two', 2)]</code></p> +<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#list">list</a> or <a href="https://docs.python.org/library/stdtypes.html#tuple">tuple</a> literals. They are converted using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function and possible tuples converted further to lists. They can contain any values <code>ast.literal_eval</code> supports, including lists and other collections.</p> +<p>If the argument is a list, it is used without conversion. Tuples and other sequences are converted to lists.</p> +<p>If the type has nested types like <code>list[int]</code>, items are converted to those types automatically.</p> +<p>Examples: <code>['one', 'two']</code>, <code>[('one', 1), ('two', 2)]</code></p> +<p>Support to convert nested types is new in Robot Framework 6.0. Support for tuple literals is new in Robot Framework 7.4.</p> string Sequence @@ -837,14 +987,35 @@ have changed and that other properties are still at their pre-patch values.Get Ids From Url + +<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#dict">dictionary</a> literals. They are converted to actual dictionaries using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function. They can contain any values <code>ast.literal_eval</code> supports, including dictionaries and other collections.</p> +<p>Any mapping is accepted without conversion. An exception is that if the type is <code>MutableMapping</code>, immutable values are converted to <code>dict</code>.</p> +<p>If the type has nested types like <code>Mapping[str, int]</code>, items are converted to those types automatically. This in new in Robot Framework 6.0.</p> +<p>Examples: <code>{'a': 1, 'b': 2}</code>, <code>{'key': 1, 'nested': {'key': 2}}</code></p> + +string +Mapping + + +__init__ +Get Request Values +Perform Validated Request +Validate Send Response +Validated Request + + -<p>String <code>NONE</code> (case-insensitive) is converted to Python <code>None</code> object. Other values cause an error.</p> +<p>String <code>NONE</code> (case-insensitive) and the empty string are converted to the Python <code>None</code> object. Other values cause an error.</p> +<p>Converting the empty string is new in Robot Framework 7.4.</p> string __init__ Authorized Request +Get Request Values Object +Perform Authorized Request +Validated Request @@ -859,7 +1030,9 @@ have changed and that other properties are still at their pre-patch values. -<p>All arguments are converted to Unicode strings.</p> +<p>All arguments are converted to Unicode strings.</p> +<p>Most values are converted simply by using <code>str(value)</code>. An exception is that bytes are mapped directly to Unicode code points with same ordinals. This means that, for example, <code>b"hyv\xe4"</code> becomes <code>"hyvä"</code>.</p> +<p>Converting bytes specially is new Robot Framework 7.4.</p> Any @@ -867,6 +1040,7 @@ have changed and that other properties are still at their pre-patch values.__init__ Assert Href To Resource Is Valid Authorized Request +Convert Request Values To Dict Ensure In Use Get Ids From Url Get Invalid Body Data @@ -876,6 +1050,7 @@ have changed and that other properties are still at their pre-patch values.Get Parameterized Path From Url Get Request Data Get Request Values +Get Request Values Object Get Valid Id For Path Get Valid Url Perform Validated Request @@ -885,12 +1060,15 @@ have changed and that other properties are still at their pre-patch values.Set Security Token Validate Response Validate Send Response +Validated Request -<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#tuple">tuple</a> literals. They are converted to actual tuples using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function. They can contain any values <code>ast.literal_eval</code> supports, including tuples and other containers.</p> -<p>If the type has nested types like <code>tuple[str, int, int]</code>, items are converted to those types automatically. This in new in Robot Framework 6.0.</p> -<p>Examples: <code>('one', 'two')</code>, <code>(('one', 1), ('two', 2))</code></p> +<p>Strings must be Python <a href="https://docs.python.org/library/stdtypes.html#tuple">tuple</a> or <a href="https://docs.python.org/library/stdtypes.html#list">list</a> literals. They are converted using the <a href="https://docs.python.org/library/ast.html#ast.literal_eval">ast.literal_eval</a> function and possible lists converted further to tuples. They can contain any values <code>ast.literal_eval</code> supports, including tuples and other collections.</p> +<p>If the argument is a tuple, it is used without conversion. Lists and other sequences are converted to tuples.</p> +<p>If the type has nested types like <code>tuple[str, int, int]</code>, items are converted to those types automatically.</p> +<p>Examples: <code>('one', 'two')</code>, <code>(('one', 1), ('two', 2))</code></p> +<p>Support to convert nested types is new in Robot Framework 6.0. Support for list literals is new in Robot Framework 7.4.</p> string Sequence diff --git a/src/OpenApiLibCore/openapi_libcore.py b/src/OpenApiLibCore/openapi_libcore.py index 36ce83c..47eb281 100644 --- a/src/OpenApiLibCore/openapi_libcore.py +++ b/src/OpenApiLibCore/openapi_libcore.py @@ -1,13 +1,15 @@ import json as _json import sys +import tempfile from collections.abc import Mapping, MutableMapping from copy import deepcopy from functools import cached_property from pathlib import Path from types import MappingProxyType -from typing import Any, Generator +from typing import Any, Callable, Generator, Literal, Sequence, overload -from openapi_core import Config, OpenAPI, Spec +from jsonschema_path import SchemaPath +from openapi_core import Config, OpenAPI from openapi_core.validation.exceptions import ValidationError from prance import ResolvingParser from prance.util.url import ResolutionError @@ -19,32 +21,33 @@ from robot.api.exceptions import FatalError from robot.libraries.BuiltIn import BuiltIn -import OpenApiLibCore.data_generation as _data_generation -import OpenApiLibCore.data_invalidation as _data_invalidation -import OpenApiLibCore.path_functions as _path_functions -import OpenApiLibCore.path_invalidation as _path_invalidation -import OpenApiLibCore.resource_relations as _resource_relations -import OpenApiLibCore.validation as _validation +import OpenApiLibCore.data_generation.data_generation_core as _data_generation +import OpenApiLibCore.data_generation.data_invalidation as _data_invalidation +import OpenApiLibCore.keyword_logic.path_functions as _path_functions +import OpenApiLibCore.keyword_logic.path_invalidation as _path_invalidation +import OpenApiLibCore.keyword_logic.resource_relations as _resource_relations +import OpenApiLibCore.keyword_logic.validation as _validation from OpenApiLibCore.annotations import JSON -from OpenApiLibCore.dto_base import Dto, IdReference -from OpenApiLibCore.dto_utils import ( - DEFAULT_ID_PROPERTY_NAME, - get_dto_class, +from OpenApiLibCore.data_generation.localized_faker import FAKE +from OpenApiLibCore.data_relations.relations_base import ( + RelationsMapping, get_id_property_name, - get_path_dto_class, + get_path_mapping_dict, + get_relations_mapping_dict, ) -from OpenApiLibCore.localized_faker import FAKE -from OpenApiLibCore.models import ( +from OpenApiLibCore.models.oas_models import ( OpenApiObject, + ParameterObject, PathItemObject, ) -from OpenApiLibCore.oas_cache import PARSER_CACHE, CachedParser -from OpenApiLibCore.parameter_utils import ( +from OpenApiLibCore.models.request_data import RequestData, RequestValues +from OpenApiLibCore.models.resource_relations import IdReference +from OpenApiLibCore.protocols import IResponseValidator +from OpenApiLibCore.utils.oas_cache import SPEC_CACHE, CachedSpec +from OpenApiLibCore.utils.parameter_utils import ( get_oas_name_from_safe_name, - register_path_parameters, + get_safe_name_for_oas_name, ) -from OpenApiLibCore.protocols import ResponseValidatorType -from OpenApiLibCore.request_data import RequestData, RequestValues from openapitools_docs.docstrings import ( OPENAPILIBCORE_INIT_DOCSTRING, OPENAPILIBCORE_LIBRARY_DOCSTRING, @@ -55,7 +58,40 @@ default_json_mapping: Mapping[str, JSON] = MappingProxyType({}) -@library(scope="SUITE", doc_format="HTML") +@overload +def _run_keyword( + keyword_name: Literal["get_valid_url"], *args: str +) -> str: ... # pragma: no cover + + +@overload +def _run_keyword( + keyword_name: Literal["get_request_data"], *args: str +) -> RequestData: ... # pragma: no cover + + +def _run_keyword(keyword_name: str, *args: object) -> object: + return run_keyword(keyword_name, *args) # pyright: ignore[reportArgumentType] + + +class LibrarySearchOrderManager: + ROBOT_LISTENER_API_VERSION = 2 + ROBOT_LIBRARY_SCOPE = "SUITE" + + def __init__(self) -> None: + self.previous_search_order: dict[str, Sequence[str]] = {} + + def start_keyword(self, name: str, attrs: dict[str, str]) -> None: + previous_search_order = BuiltIn().set_library_search_order(attrs["libname"]) + self.previous_search_order[name] = previous_search_order + + def end_keyword(self, name: str, attrs: dict[str, str]) -> None: + previous_search_order = self.previous_search_order.get(name, None) + if previous_search_order is not None: + _ = BuiltIn().set_library_search_order(*previous_search_order) + + +@library(scope="SUITE", doc_format="HTML", listener=LibrarySearchOrderManager()) class OpenApiLibCore: # pylint: disable=too-many-public-methods def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value self, @@ -65,7 +101,7 @@ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value response_validation: _validation.ValidationLevel = _validation.ValidationLevel.WARN, disable_server_validation: bool = True, mappings_path: str | Path = "", - invalid_property_default_response: int = 422, + invalid_data_default_response: int = 422, default_id_property_name: str = "id", faker_locale: str | list[str] = "", require_body_for_invalid_url: bool = False, @@ -104,7 +140,12 @@ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value self.extra_headers = extra_headers self.cookies = cookies self.proxies = proxies - self.invalid_property_default_response = invalid_property_default_response + self.invalid_data_default_response = invalid_data_default_response + if faker_locale: + FAKE.set_locale(locale=faker_locale) + self.require_body_for_invalid_url = require_body_for_invalid_url + self._server_validation_warning_logged = False + if mappings_path and str(mappings_path) != ".": mappings_path = Path(mappings_path) if not mappings_path.is_file(): @@ -114,30 +155,28 @@ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value mappings_folder = str(mappings_path.parent) sys.path.append(mappings_folder) mappings_module_name = mappings_path.stem - self.get_dto_class = get_dto_class( + self.relations_mapping_dict = get_relations_mapping_dict( mappings_module_name=mappings_module_name ) - self.get_path_dto_class = get_path_dto_class( + self.path_mapping_dict = get_path_mapping_dict( mappings_module_name=mappings_module_name ) self.get_id_property_name = get_id_property_name( - mappings_module_name=mappings_module_name + mappings_module_name=mappings_module_name, + default_id_property_name=default_id_property_name, ) sys.path.pop() else: - self.get_dto_class = get_dto_class(mappings_module_name="no mapping") - self.get_path_dto_class = get_path_dto_class( + self.relations_mapping_dict = get_relations_mapping_dict( mappings_module_name="no mapping" ) - self.get_id_property_name = get_id_property_name( + self.path_mapping_dict = get_path_mapping_dict( mappings_module_name="no mapping" ) - if faker_locale: - FAKE.set_locale(locale=faker_locale) - self.require_body_for_invalid_url = require_body_for_invalid_url - # update the globally available DEFAULT_ID_PROPERTY_NAME to the provided value - DEFAULT_ID_PROPERTY_NAME.id_property_name = default_id_property_name - self._server_validation_warning_logged = False + self.get_id_property_name = get_id_property_name( + mappings_module_name="no mapping", + default_id_property_name=default_id_property_name, + ) # region: library configuration keywords @keyword @@ -204,15 +243,20 @@ def get_request_values( method: str, overrides: Mapping[str, JSON] = default_json_mapping, ) -> RequestValues: - """Return an object with all (valid) request values needed to make a request.""" - json_data: dict[str, JSON] = {} + """ + Return an object with all (valid) request values needed to make a request. - url: str = run_keyword("get_valid_url", path) - request_data: RequestData = run_keyword("get_request_data", path, method) + The `overrides` dictionary can be used to pass specific values for parameters + instead of having them be generated automatically. + """ + json_data: JSON = {} + + url = _run_keyword("get_valid_url", path) + request_data = _run_keyword("get_request_data", path, method) params = request_data.params headers = request_data.headers if request_data.has_body: - json_data = request_data.dto.as_dict() + json_data = request_data.valid_data request_values = RequestValues( url=url, @@ -229,7 +273,9 @@ def get_request_values( if location == "body": request_values.override_body_value(name=oas_name, value=value) if location == "header": - request_values.override_header_value(name=oas_name, value=value) + request_values.override_header_value( + name=oas_name, value=str(value) + ) if location == "query": request_values.override_param_value(name=oas_name, value=str(value)) else: @@ -244,8 +290,6 @@ def get_request_data(self, path: str, method: str) -> RequestData: return _data_generation.get_request_data( path=path, method=method, - get_dto_class=self.get_dto_class, - get_id_property_name=self.get_id_property_name, openapi_spec=self.openapi_spec, ) @@ -256,10 +300,10 @@ def get_invalid_body_data( method: str, status_code: int, request_data: RequestData, - ) -> dict[str, JSON]: + ) -> JSON: """ - Return `json_data` based on the `dto` on the `request_data` that will cause - the provided `status_code` for the `method` operation on the `url`. + Return `json_data` based on the `relations_mapping` on the `request_data` that + will cause the provided `status_code` for the `method` operation on the `url`. > Note: applicable UniquePropertyValueConstraint and IdReference Relations are considered before changes to `json_data` are made. @@ -269,7 +313,7 @@ def get_invalid_body_data( method=method, status_code=status_code, request_data=request_data, - invalid_property_default_response=self.invalid_property_default_response, + invalid_data_default_response=self.invalid_data_default_response, ) @keyword @@ -277,7 +321,7 @@ def get_invalidated_parameters( self, status_code: int, request_data: RequestData, - ) -> tuple[dict[str, JSON], dict[str, JSON]]: + ) -> tuple[dict[str, JSON], dict[str, str]]: """ Returns a version of `params, headers` as present on `request_data` that has been modified to cause the provided `status_code`. @@ -285,23 +329,29 @@ def get_invalidated_parameters( return _data_invalidation.get_invalidated_parameters( status_code=status_code, request_data=request_data, - invalid_property_default_response=self.invalid_property_default_response, + invalid_data_default_response=self.invalid_data_default_response, ) @keyword def get_json_data_with_conflict( - self, url: str, method: str, dto: Dto, conflict_status_code: int + self, + url: str, + method: str, + json_data: dict[str, JSON], + relations_mapping: type[RelationsMapping], + conflict_status_code: int, ) -> dict[str, JSON]: """ Return `json_data` based on the `UniquePropertyValueConstraint` that must be - returned by the `get_relations` implementation on the `dto` for the given - `conflict_status_code`. + returned by the `get_relations` implementation on the `relations_mapping` for + the given `conflict_status_code`. """ return _data_invalidation.get_json_data_with_conflict( url=url, base_url=self.base_url, method=method, - dto=dto, + json_data=json_data, + relations_mapping=relations_mapping, # FIXME: the model should have this information conflict_status_code=conflict_status_code, ) @@ -322,7 +372,6 @@ def get_valid_url(self, path: str) -> str: return _path_functions.get_valid_url( path=path, base_url=self.base_url, - get_path_dto_class=self.get_path_dto_class, openapi_spec=self.openapi_spec, ) @@ -335,7 +384,7 @@ def get_valid_id_for_path(self, path: str) -> str | int | float: (by a POST operation) if possible. """ return _path_functions.get_valid_id_for_path( - path=path, get_id_property_name=self.get_id_property_name + path=path, openapi_spec=self.openapi_spec ) @keyword @@ -358,30 +407,26 @@ def get_ids_from_url(self, url: str) -> list[str]: Perform a GET request on the `url` and return the list of resource `ids` from the response. """ - return _path_functions.get_ids_from_url( - url=url, get_id_property_name=self.get_id_property_name - ) + return _path_functions.get_ids_from_url(url=url, openapi_spec=self.openapi_spec) @keyword def get_invalidated_url( self, valid_url: str, - path: str = "", expected_status_code: int = 404, ) -> str: """ Return an url with all the path parameters in the `valid_url` replaced by a random UUID if no PathPropertiesConstraint is mapped for the `"get"` operation - on the mapped `path` and `expected_status_code`. + on the related `path` and `expected_status_code`. If a PathPropertiesConstraint is mapped, the `invalid_value` is returned. Raises: ValueError if the valid_url cannot be invalidated. """ return _path_invalidation.get_invalidated_url( valid_url=valid_url, - path=path, base_url=self.base_url, - get_path_dto_class=self.get_path_dto_class, + openapi_spec=self.openapi_spec, expected_status_code=expected_status_code, ) @@ -414,15 +459,11 @@ def authorized_request( # pylint: disable=too-many-arguments files: Any = None, ) -> Response: """ - Perform a request using the security token or authentication set in the library. + See the `Perform Authorized Request` keyword. - `json_data`, `data` and `files` are passed to `requests.request`s `json`, - `data` and `files` parameters unaltered. - See the requests documentation for details: - https://requests.readthedocs.io/en/latest/api/#requests.request - - > Note: provided username / password or auth objects take precedence over token - based security + The difference between these keywords is that this keyword accepts separate + arguments where `Perform Validated Request` accepts a `RequestValues` object + (see the `Get Request Values` keyword for details). """ headers = deepcopy(headers) if headers else {} if self.extra_headers: @@ -449,8 +490,120 @@ def authorized_request( # pylint: disable=too-many-arguments logger.debug(f"Response text: {response.text}") return response + @keyword + def perform_authorized_request( + self, + request_values: RequestValues, + data: Any = None, + files: Any = None, + ) -> Response: + """ + Perform a request using the security token or authentication set in the library. + + `json_data`, `data` and `files` are passed to `requests.request`s `json`, + `data` and `files` parameters unaltered. + See the requests documentation for details: + https://requests.readthedocs.io/en/latest/api/#requests.request + + See also `Authorized Request` and `Get Request Values`. + + > Note: provided username / password or auth objects take precedence over token + based security + """ + return self.authorized_request( + url=request_values.url, + method=request_values.method, + params=request_values.params, + headers=request_values.headers, + json_data=request_values.json_data, + data=data, + files=files, + ) + + @keyword + def get_request_values_object( + self, + url: str, + method: str, + params: dict[str, JSON] = {}, + headers: dict[str, str] = {}, + json_data: JSON = None, + ) -> RequestValues: + """ + This keyword can be used to instantiate a RequestValues object that can be used + with the `Perform Authorized Request` and `Perform Validated Request` keywords. + + This is a utility keyword that can make certain test cases or keywords more + concise, but logging and debugging information will be more limited. + + See also the `Get Request Values` keyword. + """ + # RequestValues has methods that perform mutations on its values, so + # deepcopy to prevent mutation by reference. + params = deepcopy(params) if params else {} + headers = deepcopy(headers) if headers else {} + json_data = deepcopy(json_data) + return RequestValues( + url=url, + method=method, + params=params, + headers=headers, + json_data=json_data, + ) + + @keyword + def convert_request_values_to_dict( + self, request_values: RequestValues + ) -> dict[str, JSON]: + """Convert a RequestValues object to a dictionary.""" + return { + "url": request_values.url, + "method": request_values.method, + "params": request_values.params, + "headers": request_values.headers, + "json_data": request_values.json_data, + } + # endregion # region: validation keywords + @keyword + def validated_request( + self, + path: str, + status_code: int, + url: str, + method: str, + params: dict[str, JSON] = {}, + headers: dict[str, str] = {}, + json_data: JSON = None, + original_data: Mapping[str, JSON] = default_json_mapping, + ) -> None: + """ + See the `Perform Validated Request` keyword. + + The difference between these keywords is that this keyword accepts separate + arguments where `Perform Validated Request` accepts a `RequestValues` object + (see the `Get Request Values` keyword for details). + """ + # RequestValues has methods that perform mutations on its values, so + # deepcopy to prevent mutation by reference. + params = deepcopy(params) if params else {} + headers = deepcopy(headers) if headers else {} + json_data = deepcopy(json_data) + request_values = RequestValues( + url=url, + method=method, + params=params, + headers=headers, + json_data=json_data, + ) + _validation.perform_validated_request( + path=path, + status_code=status_code, + request_values=request_values, + original_data=original_data, + ) + @keyword def perform_validated_request( self, @@ -463,6 +616,8 @@ def perform_validated_request( This keyword first calls the Authorized Request keyword, then the Validate Response keyword and finally validates, for `DELETE` operations, whether the target resource was indeed deleted (OK response) or not (error responses). + + See also `Validated Request` and `Get Request Values`. """ _validation.perform_validated_request( path=path, @@ -474,7 +629,7 @@ def perform_validated_request( @keyword def validate_response_using_validator(self, response: Response) -> None: """ - Validate the `response` against the OpenAPI Spec that is + Validate the `response` against the OpenAPI spec that is loaded during library initialization. """ _validation.validate_response_using_validator( @@ -484,11 +639,11 @@ def validate_response_using_validator(self, response: Response) -> None: @keyword def assert_href_to_resource_is_valid( - self, href: str, referenced_resource: dict[str, JSON] + self, href: str, referenced_resource: JSON ) -> None: """ Attempt to GET the resource referenced by the `href` and validate it's equal - to the provided `referenced_resource` object / dictionary. + to the provided `referenced_resource`. """ _validation.assert_href_to_resource_is_valid( href=href, @@ -502,7 +657,7 @@ def validate_response( self, path: str, response: Response, - original_data: Mapping[str, JSON] = default_json_mapping, + original_data: JSON = default_json_mapping, # type: ignore[assignment] ) -> None: """ Validate the `response` by performing the following validations: @@ -520,7 +675,7 @@ def validate_response( response_validator=self.response_validator, server_validation_warning_logged=self._server_validation_warning_logged, disable_server_validation=self.disable_server_validation, - invalid_property_default_response=self.invalid_property_default_response, + invalid_data_default_response=self.invalid_data_default_response, response_validation=self.response_validation, openapi_spec=self.openapi_spec, original_data=original_data, @@ -552,11 +707,6 @@ def origin(self) -> str: def base_url(self) -> str: return f"{self.origin}{self._base_path}" - @cached_property - def validation_spec(self) -> Spec: - _, validation_spec, _ = self._load_specs_and_validator() - return validation_spec - @property def openapi_spec(self) -> OpenApiObject: """Return a deepcopy of the parsed openapi document.""" @@ -565,19 +715,65 @@ def openapi_spec(self) -> OpenApiObject: @cached_property def _openapi_spec(self) -> OpenApiObject: - parser, _, _ = self._load_specs_and_validator() - spec_model = OpenApiObject.model_validate(parser.specification) - register_path_parameters(spec_model.paths) + specification, _ = self._load_specs_and_validator() + spec_model = OpenApiObject.model_validate(specification) + spec_model = self._perform_post_init_model_updates(spec_model=spec_model) + self._register_path_parameters(spec_model.paths) + return spec_model + + def _register_path_parameters(self, paths_data: dict[str, PathItemObject]) -> None: + def _register_path_parameter(parameter_object: ParameterObject) -> None: + if parameter_object.in_ == "path": + _ = get_safe_name_for_oas_name(parameter_object.name) + + for path_item in paths_data.values(): + operations = path_item.operations + for operation in operations.values(): + if parameters := operation.parameters: + for parameter in parameters: + _register_path_parameter(parameter_object=parameter) + + def _perform_post_init_model_updates( + self, spec_model: OpenApiObject + ) -> OpenApiObject: + for ( + path, + operation, + ), data_relations in self.relations_mapping_dict.items(): + try: + operation_item = getattr(spec_model.paths[path], operation.lower()) + operation_item.relations_mapping = data_relations + except KeyError: + logger.warn( + f"The RELATIONS_MAPPING contains a path that is not found in the OpenAPI spec: {path}" + ) + + for path, path_relation in self.path_mapping_dict.items(): + try: + path_item = spec_model.paths[path] + path_item.relations_mapping = path_relation + except KeyError: + logger.warn( + f"The PATH_MAPPING contains a path that is not found in the OpenAPI spec: {path}" + ) + + for path, path_item in spec_model.paths.items(): + mapper = self.get_id_property_name(path) + path_item.id_mapper = mapper + path_item.update_operation_parameters() + path_item.attach_relations_mappings() + path_item.replace_nullable_with_union() + return spec_model @cached_property def response_validator( self, - ) -> ResponseValidatorType: - _, _, response_validator = self._load_specs_and_validator() + ) -> IResponseValidator: + _, response_validator = self._load_specs_and_validator() return response_validator - def _get_json_types_from_spec(self, spec: dict[str, JSON]) -> set[str]: + def _get_json_types_from_spec(self, spec: Mapping[str, JSON]) -> set[str]: json_types: set[str] = set(self._get_json_types(spec)) return {json_type for json_type in json_types if json_type is not None} @@ -601,9 +797,8 @@ def _get_json_types(self, item: object) -> Generator[str, None, None]: def _load_specs_and_validator( self, ) -> tuple[ - ResolvingParser, - Spec, - ResponseValidatorType, + Mapping[str, JSON], + IResponseValidator, ]: def recursion_limit_handler( limit: int, # pylint: disable=unused-argument @@ -613,50 +808,38 @@ def recursion_limit_handler( return self._recursion_default # pragma: no cover try: - # Since parsing of the OAS and creating the Spec can take a long time, + # Since parsing of the OAS and the specification can take a long time, # they are cached. This is done by storing them in an imported module that # will have a global scope due to how the Python import system works. This # ensures that in a Suite of Suites where multiple Suites use the same # `source`, that OAS is only parsed / loaded once. - cached_parser = PARSER_CACHE.get(self._source, None) - if cached_parser: + cached_spec = SPEC_CACHE.get(self._source, None) + if cached_spec: return ( - cached_parser.parser, - cached_parser.validation_spec, - cached_parser.response_validator, + cached_spec.specification, + cached_spec.response_validator, ) - parser = ResolvingParser( - self._source, - backend="openapi-spec-validator", - recursion_limit=self._recursion_limit, - recursion_limit_handler=recursion_limit_handler, - ) + specification = self._get_specification(recursion_limit_handler) - if parser.specification is None: # pragma: no cover - raise FatalError( - "Source was loaded, but no specification was present after parsing." - ) - - validation_spec = Spec.from_dict(parser.specification) # pyright: ignore[reportArgumentType] + validation_spec = SchemaPath.from_dict(specification) # type: ignore[arg-type] json_types_from_spec: set[str] = self._get_json_types_from_spec( - parser.specification + specification ) extra_deserializers = { json_type: _json.loads for json_type in json_types_from_spec } config = Config(extra_media_type_deserializers=extra_deserializers) # type: ignore[arg-type] openapi = OpenAPI(spec=validation_spec, config=config) - response_validator: ResponseValidatorType = openapi.validate_response # type: ignore[assignment] + response_validator: IResponseValidator = openapi.validate_response - PARSER_CACHE[self._source] = CachedParser( - parser=parser, - validation_spec=validation_spec, + SPEC_CACHE[self._source] = CachedSpec( + specification=specification, response_validator=response_validator, ) - return parser, validation_spec, response_validator + return specification, response_validator except ResolutionError as exception: # pragma: no cover raise FatalError( @@ -667,6 +850,62 @@ def recursion_limit_handler( f"ValidationError while trying to load openapi spec: {exception}" ) from exception + def _get_specification( + self, recursion_limit_handler: Callable[[int, str, JSON], JSON] + ) -> Mapping[str, JSON]: + if Path(self._source).is_file(): + return self._load_specification( + filepath=self._source, recursion_limit_handler=recursion_limit_handler + ) + + try: + response = self.authorized_request(url=self._source, method="GET") + response.raise_for_status() + except Exception as exception: # pragma: no cover + raise FatalError( + f"Failed to download the OpenAPI spec using an authorized request." + f"\nThis download attempt was made since the provided `source` " + f"does not point to a file.\nPlease verify the source path is " + f"correct if you intent to reference a local file. " + f"\nMake sure the source url is correct and reachable if " + f"referencing a web resource." + f"\nThe exception was: {exception}" + ) + + _, _, filename = self._source.rpartition("/") + with tempfile.TemporaryDirectory() as tempdir: + filepath = Path(tempdir, filename) + with open(file=filepath, mode="w", encoding="UTF-8") as spec_file: + spec_file.write(response.text) + + return self._load_specification( + filepath=filepath.as_posix(), + recursion_limit_handler=recursion_limit_handler, + ) + + def _load_specification( + self, filepath: str, recursion_limit_handler: Callable[[int, str, JSON], JSON] + ) -> Mapping[str, JSON]: + try: + parser = ResolvingParser( + filepath, + backend="openapi-spec-validator", + recursion_limit=self._recursion_limit, + recursion_limit_handler=recursion_limit_handler, + ) # type: ignore[no-untyped-call] + except Exception as exception: # pragma: no cover + raise FatalError( + f"Failed to parse the OpenAPI spec downloaded to {filepath}" + f"\nThe exception was: {exception}" + ) + + if parser.specification is None: # pragma: no cover + raise FatalError( + "Source was loaded, but no specification was present after parsing." + ) + + return parser.specification # type: ignore[no-any-return] + def read_paths(self) -> dict[str, PathItemObject]: return self.openapi_spec.paths diff --git a/src/OpenApiLibCore/protocols.py b/src/OpenApiLibCore/protocols.py index 40958f7..ca9b877 100644 --- a/src/OpenApiLibCore/protocols.py +++ b/src/OpenApiLibCore/protocols.py @@ -1,38 +1,72 @@ """A module holding Protcols.""" -from typing import Callable, Protocol, Type +from __future__ import annotations + +import builtins +from typing import Any, Callable, Protocol from openapi_core.contrib.requests import ( RequestsOpenAPIRequest, RequestsOpenAPIResponse, ) +from pydantic import GetCoreSchemaHandler +from pydantic_core import CoreSchema, core_schema -from OpenApiLibCore.dto_base import Dto +from OpenApiLibCore.models.resource_relations import ( + PathPropertiesConstraint, + ResourceRelation, +) -class ResponseValidatorType(Protocol): +class IResponseValidator(Protocol): def __call__( self, request: RequestsOpenAPIRequest, response: RequestsOpenAPIResponse - ) -> None: ... # pragma: no cover + ) -> None: ... -class GetDtoClassType(Protocol): - def __init__(self, mappings_module_name: str) -> None: ... # pragma: no cover +class IGetIdPropertyName(Protocol): + def __init__( + self, mappings_module_name: str, default_id_property_name: str + ) -> None: ... - def __call__(self, path: str, method: str) -> Type[Dto]: ... # pragma: no cover + def __call__(self, path: str) -> tuple[str, Callable[[str], str]]: ... + @property + def default_id_property_name(self) -> str: ... -class GetIdPropertyNameType(Protocol): - def __init__(self, mappings_module_name: str) -> None: ... # pragma: no cover + @property + def id_mapping( + self, + ) -> dict[str, str | tuple[str, Callable[[str], str]]]: ... - def __call__( - self, path: str - ) -> tuple[ - str, Callable[[str], str] | Callable[[int], int] - ]: ... # pragma: no cover +class IRelationsMapping(Protocol): + # NOTE: This Protocol is used as annotation in a number of the oas_models, which + # requires this method to prevent a PydanticSchemaGenerationError. + @classmethod + def __get_pydantic_core_schema__( + cls, source_type: Any, handler: GetCoreSchemaHandler + ) -> CoreSchema: + return core_schema.no_info_after_validator_function(cls, handler(str)) + + @staticmethod + def get_path_relations() -> list[PathPropertiesConstraint]: ... + + @staticmethod + def get_parameter_relations() -> list[ResourceRelation]: ... + + @classmethod + def get_parameter_relations_for_error_code( + cls, error_code: int + ) -> list[ResourceRelation]: ... + + @staticmethod + def get_relations() -> list[ResourceRelation]: ... + + @classmethod + def get_body_relations_for_error_code( + cls, error_code: int + ) -> list[ResourceRelation]: ... -class GetPathDtoClassType(Protocol): - def __init__(self, mappings_module_name: str) -> None: ... # pragma: no cover - def __call__(self, path: str) -> Type[Dto]: ... # pragma: no cover +RelationsMappingType = builtins.type[IRelationsMapping] diff --git a/src/OpenApiLibCore/resource_relations.py b/src/OpenApiLibCore/resource_relations.py deleted file mode 100644 index 600ff03..0000000 --- a/src/OpenApiLibCore/resource_relations.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Module holding the functions related to relations between resources.""" - -from requests import Response -from robot.api import logger -from robot.libraries.BuiltIn import BuiltIn - -import OpenApiLibCore.path_functions as _path_functions -from OpenApiLibCore.dto_base import IdReference -from OpenApiLibCore.models import OpenApiObject -from OpenApiLibCore.request_data import RequestData - -run_keyword = BuiltIn().run_keyword - - -def ensure_in_use( - url: str, - base_url: str, - openapi_spec: OpenApiObject, - resource_relation: IdReference, -) -> None: - resource_id = "" - - path = url.replace(base_url, "") - path_parts = path.split("/") - parameterized_path = _path_functions.get_parametrized_path( - path=path, openapi_spec=openapi_spec - ) - parameterized_path_parts = parameterized_path.split("/") - for part, param_part in zip( - reversed(path_parts), reversed(parameterized_path_parts) - ): - if param_part.endswith("}"): - resource_id = part - break - if not resource_id: - raise ValueError(f"The provided url ({url}) does not contain an id.") - request_data: RequestData = run_keyword( - "get_request_data", resource_relation.post_path, "post" - ) - json_data = request_data.dto.as_dict() - json_data[resource_relation.property_name] = resource_id - post_url: str = run_keyword("get_valid_url", resource_relation.post_path) - response: Response = run_keyword( - "authorized_request", - post_url, - "post", - request_data.params, - request_data.headers, - json_data, - ) - if not response.ok: - logger.debug( - f"POST on {post_url} with json {json_data} failed: {response.json()}" - ) - response.raise_for_status() diff --git a/src/OpenApiLibCore/utils/__init__.py b/src/OpenApiLibCore/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/OpenApiLibCore/utils/id_mapping.py b/src/OpenApiLibCore/utils/id_mapping.py new file mode 100644 index 0000000..e2f12ab --- /dev/null +++ b/src/OpenApiLibCore/utils/id_mapping.py @@ -0,0 +1,2 @@ +def dummy_transformer(valid_id: str) -> str: + return valid_id diff --git a/src/OpenApiLibCore/utils/oas_cache.py b/src/OpenApiLibCore/utils/oas_cache.py new file mode 100644 index 0000000..25bbd11 --- /dev/null +++ b/src/OpenApiLibCore/utils/oas_cache.py @@ -0,0 +1,16 @@ +"""Module holding the (global) spec cache.""" + +from dataclasses import dataclass +from typing import Mapping + +from OpenApiLibCore.annotations import JSON +from OpenApiLibCore.protocols import IResponseValidator + + +@dataclass +class CachedSpec: + specification: Mapping[str, JSON] + response_validator: IResponseValidator + + +SPEC_CACHE: dict[str, CachedSpec] = {} diff --git a/src/OpenApiLibCore/parameter_utils.py b/src/OpenApiLibCore/utils/parameter_utils.py similarity index 78% rename from src/OpenApiLibCore/parameter_utils.py rename to src/OpenApiLibCore/utils/parameter_utils.py index 4ff258c..5b2d6fe 100644 --- a/src/OpenApiLibCore/parameter_utils.py +++ b/src/OpenApiLibCore/utils/parameter_utils.py @@ -5,8 +5,6 @@ from typing import Generator -from OpenApiLibCore.models import ParameterObject, PathItemObject - PARAMETER_REGISTRY: dict[str, str] = { "body": "body", } @@ -72,22 +70,9 @@ def _convert_string_to_python_identifier() -> Generator[str, None, None]: yield f"_{ascii_code}_" if _is_python_safe(string): - return string + return string # pragma: no cover converted_string = "".join(_convert_string_to_python_identifier()) - if not _is_python_safe(converted_string): + if not _is_python_safe(converted_string): # pragma: no cover raise ValueError(f"Failed to convert '{string}' to Python identifier.") return converted_string - - -def register_path_parameters(paths_data: dict[str, PathItemObject]) -> None: - def _register_path_parameter(parameter_object: ParameterObject) -> None: - if parameter_object.in_ == "path": - _ = get_safe_name_for_oas_name(parameter_object.name) - - for path_item in paths_data.values(): - operations = path_item.get_operations() - for operation in operations.values(): - if parameters := operation.parameters: - for parameter in parameters: - _register_path_parameter(parameter_object=parameter) diff --git a/src/OpenApiLibCore/value_utils.py b/src/OpenApiLibCore/value_utils.py deleted file mode 100644 index ec133f8..0000000 --- a/src/OpenApiLibCore/value_utils.py +++ /dev/null @@ -1,216 +0,0 @@ -# mypy: disable-error-code=no-any-return -"""Utility module with functions to handle OpenAPI value types and restrictions.""" - -from copy import deepcopy -from random import choice -from typing import Any, Iterable, cast, overload - -from OpenApiLibCore.annotations import JSON -from OpenApiLibCore.localized_faker import FAKE -from OpenApiLibCore.models import ResolvedSchemaObjectTypes - - -class Ignore: - """Helper class to flag properties to be ignored in data generation.""" - - def __str__(self) -> str: - return "IGNORE" - - -class UnSet: - """Helper class to flag arguments that have not been set in a keyword call.""" - - def __str__(self) -> str: - return "UNSET" - - -IGNORE = Ignore() - -UNSET = UnSet() - - -def json_type_name_of_python_type(python_type: Any) -> str: - """Return the JSON type name for supported Python types.""" - if python_type == str: - return "string" - if python_type == bool: - return "boolean" - if python_type == int: - return "integer" - if python_type == float: - return "number" - if python_type == list: - return "array" - if python_type == dict: - return "object" - if python_type == type(None): - return "null" - raise ValueError(f"No json type mapping for Python type {python_type} available.") - - -def python_type_by_json_type_name(type_name: str) -> type: - """Return the Python type based on the JSON type name.""" - if type_name == "string": - return str - if type_name == "boolean": - return bool - if type_name == "integer": - return int - if type_name == "number": - return float - if type_name == "array": - return list - if type_name == "object": - return dict - if type_name == "null": - return type(None) - raise ValueError(f"No Python type mapping for JSON type '{type_name}' available.") - - -def get_invalid_value( - value_schema: ResolvedSchemaObjectTypes, - current_value: JSON, - values_from_constraint: Iterable[JSON] = tuple(), -) -> JSON | Ignore: - """Return a random value that violates the provided value_schema.""" - invalid_values: list[JSON | Ignore] = [] - value_type = value_schema.type - - if not isinstance(current_value, python_type_by_json_type_name(value_type)): - current_value = value_schema.get_valid_value() - - if values_from_constraint: - try: - return get_invalid_value_from_constraint( - values_from_constraint=list(values_from_constraint), - value_type=value_type, - ) - except ValueError: - pass - - # For schemas with a const or enum, add invalidated values from those - try: - invalid_value = value_schema.get_invalid_value_from_const_or_enum() - invalid_values.append(invalid_value) - except ValueError: - pass - - # Violate min / max values or length if possible - try: - values_out_of_bounds = value_schema.get_values_out_of_bounds( - current_value=current_value # type: ignore[arg-type] - ) - invalid_values += values_out_of_bounds - except ValueError: - pass - - # No value constraints or min / max ranges to violate, so change the data type - if value_type == "string": - # Since int / float / bool can always be cast to sting, change - # the string to a nested object. - # An array gets exploded in query strings, "null" is then often invalid - invalid_values.append([{"invalid": [None, False]}, "null", None, True]) - else: - invalid_values.append(FAKE.uuid()) - - return choice(invalid_values) - - -def get_invalid_value_from_constraint( - values_from_constraint: list[JSON | Ignore], value_type: str -) -> JSON | Ignore: - """ - Return a value of the same type as the values in the values_from_constraints that - is not in the values_from_constraints, if possible. Otherwise returns None. - """ - # if IGNORE is in the values_from_constraints, the parameter needs to be - # ignored for an OK response so leaving the value at it's original value - # should result in the specified error response - if any(map(lambda x: isinstance(x, Ignore), values_from_constraint)): - return IGNORE - # if the value is forced True or False, return the opposite to invalidate - if len(values_from_constraint) == 1 and value_type == "boolean": - return not values_from_constraint[0] - # for unsupported types or empty constraints lists raise a ValueError - if ( - value_type not in ["string", "integer", "number", "array", "object"] - or not values_from_constraint - ): - raise ValueError( - f"Cannot get invalid value for {value_type} from {values_from_constraint}" - ) - - values_from_constraint = deepcopy(values_from_constraint) - # for objects, keep the keys intact but update the values - if value_type == "object": - valid_object = cast(dict[str, JSON], values_from_constraint.pop()) - invalid_object: dict[str, JSON] = {} - for key, value in valid_object.items(): - python_type_of_value = type(value) - json_type_of_value = json_type_name_of_python_type(python_type_of_value) - invalid_value = cast( - JSON, - get_invalid_value_from_constraint( - values_from_constraint=[value], - value_type=json_type_of_value, - ), - ) - invalid_object[key] = invalid_value - return invalid_object - - # for arrays, update each value in the array to a value of the same type - if value_type == "array": - valid_array = cast(list[JSON], values_from_constraint.pop()) - invalid_array: list[JSON] = [] - for value in valid_array: - python_type_of_value = type(value) - json_type_of_value = json_type_name_of_python_type(python_type_of_value) - invalid_value = cast( - JSON, - get_invalid_value_from_constraint( - values_from_constraint=[value], - value_type=json_type_of_value, - ), - ) - invalid_array.append(invalid_value) - return invalid_array - - if value_type in ["integer", "number"]: - int_or_number_list = cast(list[int | float], values_from_constraint) - return get_invalid_int_or_number(values_from_constraint=int_or_number_list) - - str_or_bytes_list = cast(list[str] | list[bytes], values_from_constraint) - invalid_value = get_invalid_str_or_bytes(values_from_constraint=str_or_bytes_list) - if not invalid_value: - raise ValueError("Value invalidation yielded an empty string.") - return invalid_value - - -def get_invalid_int_or_number(values_from_constraint: list[int | float]) -> int | float: - invalid_values = 2 * values_from_constraint - invalid_value = invalid_values.pop() - for value in invalid_values: - invalid_value = abs(invalid_value) + abs(value) - if not invalid_value: - invalid_value += 1 - return invalid_value - - -@overload -def get_invalid_str_or_bytes( - values_from_constraint: list[str], -) -> str: ... # pragma: no cover - - -@overload -def get_invalid_str_or_bytes( - values_from_constraint: list[bytes], -) -> bytes: ... # pragma: no cover - - -def get_invalid_str_or_bytes(values_from_constraint: list[Any]) -> Any: - invalid_values = 2 * values_from_constraint - invalid_value = invalid_values.pop() - for value in invalid_values: - invalid_value = invalid_value + value - return invalid_value diff --git a/src/openapi_libgen/command_line.py b/src/openapi_libgen/command_line.py index 38f8039..c675419 100644 --- a/src/openapi_libgen/command_line.py +++ b/src/openapi_libgen/command_line.py @@ -72,7 +72,7 @@ def main() -> None: default_module_name, ) - use_summary = getenv("USE_SUMMARY_AS_KEYWORD_NAME") + use_summary: str | bool | None = getenv("USE_SUMMARY_AS_KEYWORD_NAME") if use_summary is None: if args.use_summary_as_keyword_name is None: use_summary = input( @@ -80,7 +80,7 @@ def main() -> None: ) use_summary = True if use_summary.lower().startswith("y") else False - expand_body = getenv("EXPAND_BODY_ARGUMENTS") + expand_body: str | bool | None = getenv("EXPAND_BODY_ARGUMENTS") if expand_body is None: if args.expand_body_arguments is None: expand_body = input( @@ -93,6 +93,6 @@ def main() -> None: output_folder=path, library_name=safe_library_name, module_name=safe_module_name, - use_summary=is_truthy(use_summary), - expand_body=is_truthy(expand_body), + use_summary=is_truthy(use_summary), # type: ignore[no-untyped-call] + expand_body=is_truthy(expand_body), # type: ignore[no-untyped-call] ) diff --git a/src/openapi_libgen/generator.py b/src/openapi_libgen/generator.py index 2114c2a..82e8434 100644 --- a/src/openapi_libgen/generator.py +++ b/src/openapi_libgen/generator.py @@ -7,7 +7,7 @@ from robot.utils import is_truthy from openapi_libgen.spec_parser import get_keyword_data -from OpenApiLibCore.models import OpenApiObject +from OpenApiLibCore.models.oas_models import OpenApiObject HERE = Path(__file__).parent.resolve() @@ -25,7 +25,7 @@ def recursion_limit_handler( backend="openapi-spec-validator", recursion_limit=recursion_limit, recursion_limit_handler=recursion_limit_handler, - ) + ) # type: ignore[no-untyped-call] assert parser.specification is not None, ( "Source was loaded, but no specification was present after parsing." ) @@ -81,11 +81,11 @@ def generate( use_summary = getenv("USE_SUMMARY_AS_KEYWORD_NAME") use_summary = use_summary if use_summary is not None else sys.argv[5] - use_summary = is_truthy(use_summary) + use_summary = is_truthy(use_summary) # type: ignore[no-untyped-call] expand_body = getenv("EXPAND_BODY_ARGUMENTS") expand_body = expand_body if expand_body is not None else sys.argv[6] - expand_body = is_truthy(expand_body) + expand_body = is_truthy(expand_body) # type: ignore[no-untyped-call] spec = load_openapi_spec(source=source, recursion_limit=1, recursion_default={}) diff --git a/src/openapi_libgen/spec_parser.py b/src/openapi_libgen/spec_parser.py index e8b8c7b..8b4ad8f 100644 --- a/src/openapi_libgen/spec_parser.py +++ b/src/openapi_libgen/spec_parser.py @@ -2,14 +2,14 @@ from typing import Generator from openapi_libgen.parsing_utils import remove_unsafe_characters_from_string -from OpenApiLibCore.models import ( +from OpenApiLibCore.models.oas_models import ( ObjectSchema, OpenApiObject, OperationObject, PathItemObject, SchemaObjectTypes, ) -from OpenApiLibCore.parameter_utils import get_safe_name_for_oas_name +from OpenApiLibCore.utils.parameter_utils import get_safe_name_for_oas_name KEYWORD_TEMPLATE = r"""@keyword {signature} @@ -47,7 +47,7 @@ def get_path_items( paths: dict[str, PathItemObject], ) -> Generator[OperationDetails, None, None]: for path, path_item_object in paths.items(): - operations = path_item_object.get_operations() + operations = path_item_object.operations for method, operation_object in operations.items(): operation_details = OperationDetails( path=path, diff --git a/src/openapi_libgen/templates/library.jinja b/src/openapi_libgen/templates/library.jinja index 5dd486c..11a91f2 100644 --- a/src/openapi_libgen/templates/library.jinja +++ b/src/openapi_libgen/templates/library.jinja @@ -6,7 +6,8 @@ from robot.api.deco import keyword, library from robot.libraries.BuiltIn import BuiltIn from OpenApiLibCore import UNSET, OpenApiLibCore, RequestValues -from OpenApiLibCore.path_functions import substitute_path_parameters +from OpenApiLibCore.annotations import JSON +from OpenApiLibCore.keyword_logic.path_functions import substitute_path_parameters run_keyword = BuiltIn().run_keyword @@ -20,7 +21,7 @@ class {{ library_name }}(OpenApiLibCore): @staticmethod def _perform_request(request_values: RequestValues) -> Response: response: Response = run_keyword( - "authorized_request", + "Authorized Request", request_values.url, request_values.method, request_values.params, diff --git a/src/openapitools_docs/docstrings.py b/src/openapitools_docs/docstrings.py index 20e9bff..05102f7 100644 --- a/src/openapitools_docs/docstrings.py +++ b/src/openapitools_docs/docstrings.py @@ -170,7 +170,7 @@

    mappings_path

    See the Advanced Use tab for an in-depth explanation. -

    invalid_property_default_response

    +

    invalid_data_default_response

    The default response code for requests with a JSON body that does not comply with the schema. Example: a value outside the specified range or a string value @@ -498,10 +498,10 @@
    
     from OpenApiLibCore import (
         IGNORE,
    -    Dto,
         IdDependency,
         IdReference,
         PathPropertiesConstraint,
    +    RelationsMapping,
         PropertyValueConstraint,
         UniquePropertyValueConstraint,
     )
    @@ -512,7 +512,7 @@
     }
     
     
    -class MyDtoThatDoesNothing(Dto):
    +class MyMappingThatDoesNothing(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = []
    @@ -529,13 +529,13 @@ def get_parameter_relations():
             return relations
     
     
    -DTO_MAPPING = {
    -    ("/myspecialpath", "post"): MyDtoThatDoesNothing
    +RELATIONS_MAPPING = {
    +    ("/myspecialpath", "post"): MyMappingThatDoesNothing
     }
     
     
     PATH_MAPPING = {
    -    "/mypathwithexternalid/{external_id}": MyDtoThatDoesNothing
    +    "/mypathwithexternalid/{external_id}": MyMappingThatDoesNothing
     }
     
     
    @@ -547,13 +547,13 @@ def get_parameter_relations(): Here the classes needed to implement custom mappings are imported. This section can just be copied without changes.
  • The ID_MAPPING "constant" definition / assignment.
  • -
  • The section defining the mapping Dtos. More on this later.
  • -
  • The DTO_MAPPING "constant" definition / assignment.
  • +
  • The section defining the RelationsMappings. More on this later.
  • +
  • The RELATIONS_MAPPING "constant" definition / assignment.
  • The PATH_MAPPING "constant" definition / assignment.
  • -

    The ID_MAPPING, DTO_MAPPING and PATH_MAPPING

    -When a custom mappings file is used, the OpenApiLibCore will attempt to import it and then import DTO_MAPPING, PATH_MAPPING and ID_MAPPING from it. +

    The ID_MAPPING, RELATIONS_MAPPING and PATH_MAPPING

    +When a custom mappings file is used, the OpenApiLibCore will attempt to import it and then import RELATIONS_MAPPING, PATH_MAPPING and ID_MAPPING from it. For this reason, the exact same name must be used in a custom mappings file (capitilization matters).

    The ID_MAPPING

    @@ -575,18 +575,18 @@ def my_transformer(identifier_name: str) -> str: -

    The DTO_MAPPING

    -The DTO_MAPPING is a dictionary with a tuple as its key and a mappings Dto as its value. +

    The RELATIONS_MAPPING

    +The RELATIONS_MAPPING is a dictionary with a tuple as its key and a RelationsMapping as its value. The tuple must be in the form ("path_from_the_paths_section", "method_supported_by_the_path"). The path_from_the_paths_section must be exactly as found in the openapi document. The method_supported_by_the_path must be one of the methods supported by the path and must be in lowercase.

    The PATH_MAPPING

    -The PATH_MAPPING is a dictionary with a "path_from_the_paths_section" as its key and a mappings Dto as its value. +The PATH_MAPPING is a dictionary with a "path_from_the_paths_section" as its key and a RelationsMapping as its value. The path_from_the_paths_section must be exactly as found in the openapi document. -

    Dto mapping classes

    +

    RelationsMapping classes

    As can be seen from the import section above, a number of classes are available to deal with relations between resources and / or constraints on properties. Each of these classes is designed to handle a relation or constraint commonly seen in REST APIs. @@ -611,7 +611,7 @@ def my_transformer(identifier_name: str) -> str: This relation can be implemented as follows:
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -623,8 +623,8 @@ def get_relations():
             ]
             return relations
     
    -DTO_MAPPING = {
    -    ("/employees", "post"): EmployeeDto
    +RELATIONS_MAPPING = {
    +    ("/employees", "post"): EmployeeMapping
     }
     
     
    @@ -655,7 +655,7 @@ def get_relations(): To verify that the specified error_code indeed occurs when attempting to delete the Wagegroup, we can implement the following dependency:
    
    -class WagegroupDto(Dto):
    +class WagegroupMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -667,8 +667,8 @@ def get_relations():
             ]
             return relations
     
    -DTO_MAPPING = {
    -    ("/wagegroups/{wagegroup_id}", "delete"): WagegroupDto
    +RELATIONS_MAPPING = {
    +    ("/wagegroups/{wagegroup_id}", "delete"): WagegroupMapping
     }
     
     
    @@ -687,7 +687,7 @@ def get_relations(): To verify that the specified error_code occurs when attempting to post an Employee with an employee_number that is already in use, we can implement the following dependency:
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -699,15 +699,15 @@ def get_relations():
             ]
             return relations
     
    -DTO_MAPPING = {
    -    ("/employees", "post"): EmployeeDto,
    -    ("/employees/${employee_id}", "put"): EmployeeDto,
    -    ("/employees/${employee_id}", "patch"): EmployeeDto,
    +RELATIONS_MAPPING = {
    +    ("/employees", "post"): EmployeeMapping,
    +    ("/employees/${employee_id}", "put"): EmployeeMapping,
    +    ("/employees/${employee_id}", "patch"): EmployeeMapping,
     }
     
     
    -Note how this example reuses the EmployeeDto to model the uniqueness constraint for all the operations (post, put and patch) that all relate to the same employee_number. +Note how this example reuses the EmployeeMapping to model the uniqueness constraint for all the operations (post, put and patch) that all relate to the same employee_number.
    @@ -721,7 +721,7 @@ def get_relations(): This type of constraint can be modeled as follows:
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -733,10 +733,10 @@ def get_relations():
             ]
             return relations
     
    -DTO_MAPPING = {
    -    ("/employees", "post"): EmployeeDto,
    -    ("/employees/${employee_id}", "put"): EmployeeDto,
    -    ("/employees/${employee_id}", "patch"): EmployeeDto,
    +RELATIONS_MAPPING = {
    +    ("/employees", "post"): EmployeeMapping,
    +    ("/employees/${employee_id}", "put"): EmployeeMapping,
    +    ("/employees/${employee_id}", "patch"): EmployeeMapping,
     }
     
     
    @@ -745,7 +745,7 @@ def get_relations(): To support additional restrictions like these, the PropertyValueConstraint supports two additional properties: error_value and invalid_value_error_code:
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -759,10 +759,10 @@ def get_relations():
             ]
             return relations
     
    -DTO_MAPPING = {
    -    ("/employees", "post"): EmployeeDto,
    -    ("/employees/${employee_id}", "put"): EmployeeDto,
    -    ("/employees/${employee_id}", "patch"): EmployeeDto,
    +RELATIONS_MAPPING = {
    +    ("/employees", "post"): EmployeeMapping,
    +    ("/employees/${employee_id}", "put"): EmployeeMapping,
    +    ("/employees/${employee_id}", "patch"): EmployeeMapping,
     }
     
     
    @@ -774,7 +774,7 @@ def get_relations(): This situation can be handled by use of the special IGNORE value (see below for other uses):
    
    -class EmployeeDto(Dto):
    +class EmployeeMapping(RelationsMapping):
         @staticmethod
         def get_relations():
             relations = [
    @@ -788,10 +788,10 @@ def get_relations():
             ]
             return relations
     
    -DTO_MAPPING = {
    -    ("/employees", "post"): EmployeeDto,
    -    ("/employees/${employee_id}", "put"): EmployeeDto,
    -    ("/employees/${employee_id}", "patch"): EmployeeDto,
    +RELATIONS_MAPPING = {
    +    ("/employees", "post"): EmployeeMapping,
    +    ("/employees/${employee_id}", "put"): EmployeeMapping,
    +    ("/employees/${employee_id}", "patch"): EmployeeMapping,
     }
     
     
    @@ -804,7 +804,7 @@ def get_relations():
    Just use this for the path
    -Note: The PathPropertiesConstraint is only applicable to the get_path_relations in a Dto and only the PATH_MAPPING uses the get_path_relations. +Note: The PathPropertiesConstraint is only applicable to the get_path_relations in a RelationsMapping and only the PATH_MAPPING uses the get_path_relations.
    To be able to automatically perform endpoint validations, the OpenApiLibCore has to construct the url for the resource from the path as found in the openapi document. @@ -824,7 +824,7 @@ def get_relations(): It should be clear that the OpenApiLibCore won't be able to acquire a valid month and date. The PathPropertiesConstraint can be used in this case:
    
    -class BirthdaysDto(Dto):
    +class BirthdaysMapping(RelationsMapping):
         @staticmethod
         def get_path_relations():
             relations = [
    @@ -833,7 +833,7 @@ def get_path_relations():
             return relations
     
     PATH_MAPPING = {
    -    "/birthdays/{month}/{date}": BirthdaysDto
    +    "/birthdays/{month}/{date}": BirthdaysMapping
     }
     
     
    @@ -853,7 +853,7 @@ def get_path_relations(): To prevent OpenApiLibCore from generating invalid combinations of path and query parameters in this type of endpoint, the IGNORE special value can be used to ensure the related query parameter is never send in a request.
    
    -class EnergyLabelDto(Dto):
    +class EnergyLabelMapping(RelationsMapping):
         @staticmethod
         def get_parameter_relations():
             relations = [
    @@ -872,8 +872,8 @@ def get_relations(:
             ]
             return relations
     
    -DTO_MAPPING = {
    -    ("/energy_label/{zipcode}/{home_number}", "get"): EnergyLabelDto,
    +RELATIONS_MAPPING = {
    +    ("/energy_label/{zipcode}/{home_number}", "get"): EnergyLabelMapping,
     }
     
     
    @@ -886,7 +886,7 @@ def get_relations(: Such situations can be handled by a mapping as shown below:

    
    -class PatchEmployeeDto(Dto):
    +class PatchEmployeeMapping(RelationsMapping):
         @staticmethod
         def get_parameter_relations() -> list[ResourceRelation]:
             relations: list[ResourceRelation] = [
    @@ -905,8 +905,8 @@ def get_parameter_relations() -> list[ResourceRelation]:
             ]
             return relations
     
    -DTO_MAPPING = {
    -    ("/employees/{employee_id}", "patch"): PatchEmployeeDto,
    +RELATIONS_MAPPING = {
    +    ("/employees/{employee_id}", "patch"): PatchEmployeeMapping,
     }
     
     
    diff --git a/src/openapitools_docs/documentation_generator.py b/src/openapitools_docs/documentation_generator.py index fde43b2..86a5e7e 100644 --- a/src/openapitools_docs/documentation_generator.py +++ b/src/openapitools_docs/documentation_generator.py @@ -26,7 +26,7 @@ def generate(output_folder: Path) -> None: libcore_documentation=OPENAPILIBCORE_DOCUMENTATION, advanced_use_documentation=ADVANCED_USE_DOCUMENTATION, ) - with open(output_file_path, mode="w", encoding="utf-8") as html_file: + with open(output_file_path, mode="w", encoding="UTF-8") as html_file: html_file.write(documentation_content) diff --git a/tests/driver/suites/load_json.robot b/tests/driver/suites/load_json.robot index b667da0..7db2da8 100644 --- a/tests/driver/suites/load_json.robot +++ b/tests/driver/suites/load_json.robot @@ -3,6 +3,7 @@ Library OpenApiDriver ... source=${ROOT}/tests/files/petstore_openapi.json ... ignored_responses=${IGNORED_RESPONSES} ... ignored_testcases=${IGNORED_TESTS} +... mappings_path=${ROOT}/tests/user_implemented # library can load with invalid mappings_path Test Template Do Nothing diff --git a/tests/driver/suites/test_endpoint_exceptions.robot b/tests/driver/suites/test_endpoint_exceptions.robot index 648ba22..a14524c 100644 --- a/tests/driver/suites/test_endpoint_exceptions.robot +++ b/tests/driver/suites/test_endpoint_exceptions.robot @@ -4,7 +4,7 @@ Library OpenApiDriver ... source=http://localhost:8000/openapi.json ... origin=http://localhost:8000 ... included_paths=${INCLUDED_PATHS} -... invalid_property_default_response=400 +... invalid_data_default_response=400 Test Template Validate Test Endpoint Keyword @@ -25,6 +25,6 @@ Validate Test Endpoint Keyword Run Keyword And Expect Error Response status_code 401 was not 200. ... Test Endpoint path=${path} method=${method} status_code=${status_code} ELSE - Run Keyword And Expect Error No Dto mapping found to cause status_code ${status_code}. + Run Keyword And Expect Error No relation found to cause status_code ${status_code}. ... Test Endpoint path=${path} method=${method} status_code=${status_code} END diff --git a/tests/driver/suites/test_forbidden_and_test_unauthorized_raise_on_unsecured_api.robot b/tests/driver/suites/test_forbidden_and_test_unauthorized_raise_on_unsecured_api.robot index 17ebb9d..c1416d1 100644 --- a/tests/driver/suites/test_forbidden_and_test_unauthorized_raise_on_unsecured_api.robot +++ b/tests/driver/suites/test_forbidden_and_test_unauthorized_raise_on_unsecured_api.robot @@ -12,7 +12,7 @@ Test Template Validate Unsecured Requests *** Variables *** -@{IGNORED_RESPONSES} 401 403 404 406 412 418 422 451 +@{IGNORED_RESPONSES} 400 401 403 404 406 412 418 422 451 *** Test Cases *** diff --git a/tests/driver/suites/test_with_altered_schema.robot b/tests/driver/suites/test_with_altered_schema.robot index 4fde0ed..5c5341b 100644 --- a/tests/driver/suites/test_with_altered_schema.robot +++ b/tests/driver/suites/test_with_altered_schema.robot @@ -24,10 +24,13 @@ Test Tags rf7 *** Variables *** @{EXPECTED_FAILURES} ... GET / 200 # Unsupported MIME type for response schema +... GET /events/ 200 # Message in schema changed to format "byte" ... GET /reactions/ 200 # /reactions/ path not implemented on API server +... GET /secret_message 200 # Message in schema changed to format "byte" ... POST /events/ 201 # added 'event_number' property to Event schema ... POST /employees 201 # added 'team' property to Employee schema ... GET /employees/{employee_id} 200 # added 'team' property to EmployeeDetails schema +... GET /energy_label/{zipcode}/{home_number} 200 # Message in schema changed to format "byte" ... PUT /wagegroups/{wagegroup_id} 200 # Unsupported MIME type for requestBody ... PUT /wagegroups/{wagegroup_id} 404 # Unsupported MIME type for requestBody ... PUT /wagegroups/{wagegroup_id} 418 # Unsupported MIME type for requestBody diff --git a/tests/files/altered_openapi.json b/tests/files/altered_openapi.json index 78b5b78..4295cd7 100644 --- a/tests/files/altered_openapi.json +++ b/tests/files/altered_openapi.json @@ -1065,7 +1065,8 @@ "properties": { "message": { "type": "string", - "title": "Message" + "format": "byte", + "title": "Binary message" } }, "type": "object", diff --git a/tests/files/nullable_schema_variations.json b/tests/files/nullable_schema_variations.json new file mode 100644 index 0000000..a91ef9b --- /dev/null +++ b/tests/files/nullable_schema_variations.json @@ -0,0 +1,372 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "test_get_request_data", + "version": "0.1.0" + }, + "paths": { + "/boolean_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "boolean", + "nullable": true + } + } + }, + "required": true + } + } + }, + "/integer_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "integer", + "nullable": true + } + } + }, + "required": true + } + } + }, + "/number_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "number", + "nullable": true + } + } + }, + "required": true + } + } + }, + "/string_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string", + "nullable": true + } + } + }, + "required": true + } + } + }, + "/array_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "number", + "nullable": true + } + } + } + }, + "required": true + } + } + }, + "/object_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "nullable": true, + "properties": { + "id": { + "type": "integer" + } + } + } + } + }, + "required": true + } + } + }, + "/oneof_first": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "integer", + "nullable": true + }, + { + "type": "string" + } + ] + } + } + }, + "required": true + } + } + }, + "/oneof_second": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "nullable": true + } + ] + } + } + }, + "required": true + } + } + }, + "/oneof_both": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "integer", + "nullable": true + }, + { + "type": "string", + "nullable": true + } + ] + } + } + }, + "required": true + } + } + }, + "/anyof_first": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "integer", + "nullable": true + }, + { + "type": "string" + } + ] + } + } + }, + "required": true + } + } + }, + "/anyof_second": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string", + "nullable": true + } + ] + } + } + }, + "required": true + } + } + }, + "/anyof_both": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "integer", + "nullable": true + }, + { + "type": "string", + "nullable": true + } + ] + } + } + }, + "required": true + } + } + }, + "/allof_one": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "nullable": true, + "properties": { + "name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "name" + ], + "additionalProperties": { + "type": "boolean" + } + }, + { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "integer", + "nullable": true + }, + { + "type": "number", + "nullable": true + } + ] + } + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + } + }, + "additionalProperties": false + } + ] + } + } + }, + "required": true + } + } + }, + "/allof_all": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "nullable": true, + "properties": { + "name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "name" + ], + "additionalProperties": { + "type": "boolean" + } + }, + { + "type": "object", + "nullable": true, + "additionalProperties": { + "oneOf": [ + { + "type": "integer", + "nullable": true + }, + { + "type": "number", + "nullable": true + } + ] + } + }, + { + "type": "object", + "nullable": true, + "properties": { + "data": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + } + }, + "additionalProperties": false + } + ] + } + } + }, + "required": true + } + } + } + } +} \ No newline at end of file diff --git a/tests/files/request_data_variations_3.0.json b/tests/files/request_data_variations_3.0.json new file mode 100644 index 0000000..71193c0 --- /dev/null +++ b/tests/files/request_data_variations_3.0.json @@ -0,0 +1,196 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "test_get_request_data", + "version": "0.1.0" + }, + "paths": { + "/boolean_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + }, + "required": true + } + } + }, + "/integer_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + }, + "required": true + } + } + }, + "/number_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "number" + } + } + }, + "required": true + } + } + }, + "/string_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + } + } + }, + "/bytes_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string", + "format": "byte" + } + } + }, + "required": true + } + } + }, + "/object_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "required": true + } + } + }, + "/array_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, + "required": true + } + } + }, + "/union_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + } + } + }, + "required": true + } + } + }, + "/array_with_union_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": { + "type": "boolean" + } + }, + { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "minItems": 1 + } + } + }, + "required": true + } + } + } + } +} \ No newline at end of file diff --git a/tests/files/request_data_variations_3.1.json b/tests/files/request_data_variations_3.1.json new file mode 100644 index 0000000..05ac391 --- /dev/null +++ b/tests/files/request_data_variations_3.1.json @@ -0,0 +1,213 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "test_get_request_data", + "version": "0.1.0" + }, + "paths": { + "/null_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "null" + } + } + }, + "required": true + } + } + }, + "/boolean_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + }, + "required": true + } + } + }, + "/integer_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + }, + "required": true + } + } + }, + "/number_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "number" + } + } + }, + "required": true + } + } + }, + "/string_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + } + } + }, + "/bytes_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string", + "format": "byte" + } + } + }, + "required": true + } + } + }, + "/object_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "required": true + } + } + }, + "/array_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, + "required": true + } + } + }, + "/union_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + }, + { + "type": "string" + } + ] + } + } + }, + "required": true + } + } + }, + "/array_with_union_schema": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": { + "type": "boolean" + } + }, + { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "minItems": 1 + } + } + }, + "required": true + } + } + } + } +} \ No newline at end of file diff --git a/tests/files/schema_with_allof.yaml b/tests/files/schema_with_allof_and_nullable.yaml similarity index 99% rename from tests/files/schema_with_allof.yaml rename to tests/files/schema_with_allof_and_nullable.yaml index d2d9232..cda2158 100644 --- a/tests/files/schema_with_allof.yaml +++ b/tests/files/schema_with_allof_and_nullable.yaml @@ -69,6 +69,7 @@ components: description: The publication date of the work. type: string format: date + nullable: true Images: title: images type: object diff --git a/tests/libcore/suites/test_faker_locale.robot b/tests/libcore/suites/test_faker_locale.robot index 70187ca..a63f545 100644 --- a/tests/libcore/suites/test_faker_locale.robot +++ b/tests/libcore/suites/test_faker_locale.robot @@ -1,7 +1,7 @@ *** Settings *** Variables ${ROOT}/tests/variables.py Library OpenApiLibCore -... source=${ROOT}/tests/files/schema_with_allof.yaml +... source=${ROOT}/tests/files/schema_with_allof_and_nullable.yaml ... origin=${ORIGIN} ... base_path=${EMPTY} ... faker_locale=zh_CN @@ -15,4 +15,4 @@ ${ORIGIN} http://localhost:8000 Test Get Request Data For Schema With allOf ${request_data}= Get Request Data path=/hypermedia method=post # this regex should match all characters in the simplified Chinese character set - Should Match Regexp ${request_data.dto.title} ^[\u4E00-\u9FA5]+$ + Should Match Regexp ${request_data.valid_data}[title] ^[\u4E00-\u9FA5]+$ diff --git a/tests/libcore/suites/test_get_invalid_json_data.robot b/tests/libcore/suites/test_get_invalid_json_data.robot index a93950e..f6b3fe7 100644 --- a/tests/libcore/suites/test_get_invalid_json_data.robot +++ b/tests/libcore/suites/test_get_invalid_json_data.robot @@ -34,13 +34,13 @@ Test Get Invalid Body Data Raises If Data Cannot Be Invalidated Test Get Invalid Body Data Based On Schema ${request_data}= Get Request Data path=/events/ method=post - Should Be Empty ${request_data.dto.get_body_relations_for_error_code(422)} + Should Be Empty ${request_data.relations_mapping.get_body_relations_for_error_code(422)} ${invalid_json}= Get Invalid Body Data ... url=none ... method=none ... status_code=422 ... request_data=${request_data} - Should Not Be Equal ${invalid_json} ${request_data.dto} + Should Not Be Equal ${invalid_json} ${request_data.valid_data} ${response}= Authorized Request ... url=${ORIGIN}/events/ method=post json_data=${invalid_json} Should Be Equal As Integers ${response.status_code} 422 @@ -52,12 +52,12 @@ Test Get Invalid Body Data For UniquePropertyValueConstraint ... method=post ... status_code=418 ... request_data=${request_data} - Should Not Be Equal ${invalid_json} ${request_data.dto} + Should Not Be Equal ${invalid_json} ${request_data.valid_data} ${response}= Authorized Request ... url=${ORIGIN}/wagegroups method=post json_data=${invalid_json} Should Be Equal As Integers ${response.status_code} 418 -Test Get Invalid Body Data For IdReference +Test Get Invalid Body Data For IdReference Returns The Original Valid Data ${url}= Get Valid Url path=/wagegroups/{wagegroup_id} ${request_data}= Get Request Data path=/wagegroups/{wagegroup_id} method=delete ${invalid_json}= Get Invalid Body Data @@ -65,7 +65,7 @@ Test Get Invalid Body Data For IdReference ... method=delete ... status_code=406 ... request_data=${request_data} - Should Not Be Equal ${invalid_json} ${request_data.dto} + Should Be Equal ${invalid_json} ${request_data.valid_data} ${response}= Authorized Request ... url=${url} method=delete json_data=${invalid_json} Should Be Equal As Integers ${response.status_code} 406 @@ -78,19 +78,19 @@ Test Get Invalid Body Data For IdDependency ... method=post ... status_code=451 ... request_data=${request_data} - Should Not Be Equal ${invalid_json} ${request_data.dto} + Should Not Be Equal ${invalid_json} ${request_data.valid_data} ${response}= Authorized Request ... url=${url} method=post json_data=${invalid_json} Should Be Equal As Integers ${response.status_code} 451 -Test Get Invalid Body Data For Dto With Other Relations +Test Get Invalid Body Data For Relation Mappings With Other Relations ${request_data}= Get Request Data path=/employees method=post ${invalid_json}= Get Invalid Body Data ... url=${ORIGIN}/employees ... method=post ... status_code=403 ... request_data=${request_data} - Should Not Be Equal ${invalid_json} ${request_data.dto} + Should Not Be Equal ${invalid_json} ${request_data.valid_data} ${response}= Authorized Request ... url=${ORIGIN}/employees method=post json_data=${invalid_json} Should Be Equal As Integers ${response.status_code} 403 @@ -98,13 +98,13 @@ Test Get Invalid Body Data For Dto With Other Relations Test Get Invalid Body Data Can Invalidate Missing Optional Parameters ${url}= Get Valid Url path=/employees/{emplyee_id} ${request_data}= Get Request Data path=/employees/{emplyee_id} method=patch - Evaluate ${request_data.dto.__dict__.clear()} is None + Evaluate ${request_data.valid_data.clear()} is None ${invalid_json}= Get Invalid Body Data ... url=${url} ... method=patch ... status_code=422 ... request_data=${request_data} - Should Not Be Equal ${invalid_json} ${request_data.dto.as_dict()} + Should Not Be Equal ${invalid_json} ${request_data.valid_data} ${response}= Authorized Request ... url=${url} method=patch json_data=${invalid_json} VAR @{expected_status_codes}= ${403} ${422} ${451} diff --git a/tests/libcore/suites/test_get_invalidated_url.robot b/tests/libcore/suites/test_get_invalidated_url.robot index 69d7649..6da306d 100644 --- a/tests/libcore/suites/test_get_invalidated_url.robot +++ b/tests/libcore/suites/test_get_invalidated_url.robot @@ -44,7 +44,6 @@ Test Get Invalidated Url For PathPropertiesConstraint Invalid Value Status Code ${url}= Get Valid Url path=/energy_label/{zipcode}/{home_number} ${invalidated}= Get Invalidated Url ... valid_url=${url} - ... path=/energy_label/{zipcode}/{home_number} ... expected_status_code=422 Should Not Be Equal ${url} ${invalidated} Should Start With ${invalidated} http://localhost:8000/energy_label/0123AA diff --git a/tests/libcore/suites/test_get_json_data_with_conflict.robot b/tests/libcore/suites/test_get_json_data_with_conflict.robot index 6e400f1..42e4198 100644 --- a/tests/libcore/suites/test_get_json_data_with_conflict.robot +++ b/tests/libcore/suites/test_get_json_data_with_conflict.robot @@ -13,12 +13,15 @@ ${ORIGIN} http://localhost:8000 *** Test Cases *** Test Get Json Data With Conflict Raises For No UniquePropertyValueConstraint + # No mapping for /wagegroups GET will yield a default relations_mapping on the request_data + ${request_data}= Get Request Data path=/wagegroups method=get ${url}= Get Valid Url path=/wagegroups Run Keyword And Expect Error ValueError: No UniquePropertyValueConstraint* ... Get Json Data With Conflict ... url=${url} ... method=post - ... dto=${DEFAULT_DTO()} + ... json_data=&{EMPTY} + ... relations_mapping=${request_data.relations_mapping} ... conflict_status_code=418 Test Get Json Data With Conflict For Post Request @@ -27,7 +30,8 @@ Test Get Json Data With Conflict For Post Request ${invalid_data}= Get Json Data With Conflict ... url=${url} ... method=post - ... dto=${request_data.dto} + ... json_data=${request_data.valid_data} + ... relations_mapping=${request_data.relations_mapping} ... conflict_status_code=418 Should Not Be Empty ${invalid_data} @@ -37,7 +41,8 @@ Test Get Json Data With Conflict For Put Request ${invalid_json}= Get Json Data With Conflict ... url=${url} ... method=put - ... dto=${request_data.dto} + ... json_data=${request_data.valid_data} + ... relations_mapping=${request_data.relations_mapping} ... conflict_status_code=418 ${response}= Authorized Request ... url=${url} method=put json_data=${invalid_json} @@ -49,7 +54,7 @@ Test Get Json Data With Conflict For Put Request # ${invalid_json}= Get Json Data With Conflict # ... url=${url} # ... method=put -# ... dto=${request_data.dto} +# ... relations_mapping=${request_data.relations_mapping} # ... conflict_status_code=418 # ${response}= Authorized Request # ... url=${url} method=put json_data=${invalid_json} diff --git a/tests/libcore/suites/test_get_request_data.robot b/tests/libcore/suites/test_get_request_data.robot index 3c317a7..8512e6a 100644 --- a/tests/libcore/suites/test_get_request_data.robot +++ b/tests/libcore/suites/test_get_request_data.robot @@ -19,23 +19,23 @@ Test Get Request Data For Invalid Method On Endpoint ${request_data}= Get Request Data path=/events/ method=delete VAR &{dict}= &{EMPTY} VAR @{list}= @{EMPTY} - Should Be Equal ${request_data.dto} ${DEFAULT_DTO()} + Should Be Equal ${request_data.relations_mapping.__doc__} DeleteEvents() Should Be Equal ${request_data.body_schema} ${NONE} Should Be Equal ${request_data.parameters} ${list} Should Be Equal ${request_data.params} ${dict} Should Be Equal ${request_data.headers} ${dict} Should Not Be True ${request_data.has_body} -Test Get Request Data For Endpoint With RequestBody +Test Get Request Data For Endpoint With Object RequestBody ${request_data}= Get Request Data path=/employees method=post VAR &{dict}= &{EMPTY} VAR @{list}= @{EMPTY} VAR @{birthdays}= 1970-07-07 1980-08-08 1990-09-09 VAR @{weekdays}= Monday Tuesday Wednesday Thursday Friday - Length Should Be ${request_data.dto.name} 36 - Length Should Be ${request_data.dto.wagegroup_id} 36 - Should Contain ${birthdays} ${request_data.dto.date_of_birth} - VAR ${generated_parttime_schedule}= ${request_data.dto.parttime_schedule} + Length Should Be ${request_data.valid_data}[name] 36 + Length Should Be ${request_data.valid_data}[wagegroup_id] 36 + Should Contain ${birthdays} ${request_data.valid_data}[date_of_birth] + VAR ${generated_parttime_schedule}= ${request_data.valid_data}[parttime_schedule] IF $generated_parttime_schedule is not None ${parttime_days}= Get From Dictionary ${generated_parttime_schedule} parttime_days Should Be True 1 <= len($parttime_days) <= 5 @@ -49,16 +49,37 @@ Test Get Request Data For Endpoint With RequestBody Should Not Be Empty ${request_data.body_schema.properties.root} Should Be Equal ${request_data.parameters} ${list} Should Be Equal ${request_data.params} ${dict} - VAR &{expected_headers}= content-type=application/json + VAR &{expected_headers}= Content-Type=application/json Should Be Equal ${request_data.headers} ${expected_headers} Should Be True ${request_data.has_body} -Test Get Request Data For Endpoint Without RequestBody But With DtoClass +Test Get Request Data For Endpoint With Array Request Body + VAR ${empty_array_seen}= ${FALSE} + VAR ${non_empty_array_seen}= ${FALSE} + WHILE not ($empty_array_seen and $non_empty_array_seen) limit=10 + ${request_data}= Get Request Data path=/events/ method=put + TRY + VAR ${first_valid_item}= ${request_data.valid_data[0]} + Dictionary Should Contain Key ${first_valid_item} message + Dictionary Should Contain Key ${first_valid_item} details + List Should Not Contain Duplicates ${first_valid_item}[details] + VAR ${non_empty_array_seen}= ${TRUE} + EXCEPT * IndexError: list index out of range type=glob + Should Be Equal ${request_data.body_schema.type} array + VAR ${empty_array_seen}= ${TRUE} + END + END + +Test Get Request Data For Endpoint Without RequestBody But With Relation Mapping ${request_data}= Get Request Data path=/wagegroups/{wagegroup_id} method=delete VAR &{dict}= &{EMPTY} - Should Be Equal As Strings ${request_data.dto} delete_wagegroup_wagegroups__wagegroup_id__delete() Should Be Equal ${request_data.body_schema} ${NONE} Should Not Be Empty ${request_data.parameters} Should Be Equal ${request_data.params} ${dict} Should Be Equal ${request_data.headers} ${dict} Should Not Be True ${request_data.has_body} + +Test Get Request Data For Endpoint With Treat As Mandatory Relation + ${request_data}= Get Request Data path=/employees method=post + VAR ${valid_data}= ${request_data.valid_data} + Should Not Be Equal ${valid_data["parttime_schedule"]} ${NONE} diff --git a/tests/libcore/suites/test_readonly.robot b/tests/libcore/suites/test_readonly.robot index 5f27bad..0a5cbc5 100644 --- a/tests/libcore/suites/test_readonly.robot +++ b/tests/libcore/suites/test_readonly.robot @@ -16,7 +16,7 @@ ${ORIGIN} http://localhost:8000 *** Test Cases *** Test ReadOnly Is Filtered From Request Data ${request_data}= Get Request Data path=/api/location method=post - VAR ${json_data}= ${request_data.dto.as_dict()} + VAR ${json_data}= ${request_data.valid_data} Should Not Contain ${json_data} id Should Contain ${json_data} locationId Should Contain ${json_data} timezone diff --git a/tests/libcore/suites/test_request_data_class.robot b/tests/libcore/suites/test_request_data_class.robot index 91532ea..ff39b00 100644 --- a/tests/libcore/suites/test_request_data_class.robot +++ b/tests/libcore/suites/test_request_data_class.robot @@ -59,8 +59,8 @@ Test Headers That Can Be Invalidated Test Get Required Properties Dict ${request_data}= Get Request Data path=/employees method=post - Should Contain ${request_data.dto.as_dict()} parttime_schedule - Should Not Be Empty ${request_data.dto.name} + Should Contain ${request_data.valid_data} parttime_schedule + Should Not Be Empty ${request_data.valid_data}[name] VAR ${required_properties}= ${request_data.get_required_properties_dict()} Should Contain ${required_properties} name # parttime_schedule is configured with treat_as_mandatory=True diff --git a/tests/libcore/suites/test_request_values.robot b/tests/libcore/suites/test_request_values.robot new file mode 100644 index 0000000..72d7776 --- /dev/null +++ b/tests/libcore/suites/test_request_values.robot @@ -0,0 +1,46 @@ +*** Settings *** +Variables ${ROOT}/tests/variables.py +Library Collections +Library OpenApiLibCore +... source=${ORIGIN}/openapi.json +... origin=${ORIGIN} +... base_path=${EMPTY} +... mappings_path=${ROOT}/tests/user_implemented/custom_user_mappings.py + +Test Tags rf7 + + +*** Variables *** +${ORIGIN} http://localhost:8000 + + +*** Test Cases *** +Test Requests Using RequestValues + ${request_values}= Get Request Values path=/employees method=POST + ${request_values_dict}= Convert Request Values To Dict ${request_values} + + ${response_using_request_values}= Perform Authorized Request request_values=${request_values} + Should Be Equal As Integers ${response_using_request_values.status_code} 201 + + ${response_using_dict}= Authorized Request &{request_values_dict} + Should Be Equal As Integers ${response_using_dict.status_code} 201 + + VAR ${response_dict_using_request_values}= ${response_using_request_values.json()} + VAR ${response_dict_using_request_values_dict}= ${response_using_dict.json()} + + Should Not Be Equal + ... ${response_dict_using_request_values}[identification] + ... ${response_dict_using_request_values_dict}[identification] + Should Not Be Equal + ... ${response_dict_using_request_values}[employee_number] + ... ${response_dict_using_request_values_dict}[employee_number] + + Remove From Dictionary ${response_dict_using_request_values} identification employee_number + Remove From Dictionary ${response_dict_using_request_values_dict} identification employee_number + + Should Be Equal ${response_dict_using_request_values} ${response_dict_using_request_values_dict} + + ${request_values_object}= Get Request Values Object &{request_values_dict} + + Perform Validated Request path=/employees status_code=201 request_values=${request_values_object} + Validated Request path=/employees status_code=201 &{request_values_dict} diff --git a/tests/libcore/suites/test_schema_variations.robot b/tests/libcore/suites/test_schema_variations.robot index b500116..c1fed57 100644 --- a/tests/libcore/suites/test_schema_variations.robot +++ b/tests/libcore/suites/test_schema_variations.robot @@ -1,7 +1,7 @@ *** Settings *** Variables ${ROOT}/tests/variables.py Library OpenApiLibCore -... source=${ROOT}/tests/files/schema_with_allof.yaml +... source=${ROOT}/tests/files/schema_with_allof_and_nullable.yaml ... origin=${ORIGIN} ... base_path=${EMPTY} @@ -17,12 +17,12 @@ Test Get Request Data For Schema With allOf ${request_data}= Get Request Data path=/hypermedia method=post VAR &{dict}= &{EMPTY} VAR @{list}= @{EMPTY} - VAR &{expected_headers}= content-type=application/hal+json - Length Should Be ${request_data.dto.isan} 36 - Length Should Be ${request_data.dto.published} 10 - Length Should Be ${request_data.dto.tags} 1 - Length Should Be ${request_data.dto.tags}[0] 36 - Length Should Be ${request_data.body_schema.properties.root} 4 + VAR &{expected_headers}= Content-Type=application/hal+json + Length Should Be ${request_data.valid_data}[isan] 36 + Length Should Be ${request_data.valid_data}[tags] 1 + Length Should Be ${request_data.valid_data}[tags][0] 36 + VAR ${resolved_schema}= ${request_data.body_schema} + Length Should Be ${resolved_schema.properties.root} 4 Should Be Equal ${request_data.parameters} ${list} Should Be Equal ${request_data.params} ${dict} Should Be Equal ${request_data.headers} ${expected_headers} diff --git a/tests/libcore/suites/test_validate_response.robot b/tests/libcore/suites/test_validate_response.robot index c74b787..8be967d 100644 --- a/tests/libcore/suites/test_validate_response.robot +++ b/tests/libcore/suites/test_validate_response.robot @@ -12,7 +12,7 @@ ${ORIGIN} http://localhost:8000 *** Test Cases *** -Test Bool Response +Test Boolean Response ${url}= Get Valid Url path=/employees/{employee_id} ${request_data}= Get Request Data path=/employees/{employee_id} method=patch ${response}= Authorized Request @@ -20,6 +20,6 @@ Test Bool Response ... method=patch ... params=${request_data.params} ... headers=${request_data.headers} - ... json_data=${request_data.dto.as_dict()} + ... json_data=${request_data.valid_data} Validate Response path=/employees/{employee_id} response=${response} diff --git a/tests/libcore/unittests/oas_model/__init__.py b/tests/libcore/unittests/oas_model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/libcore/unittests/test_can_be_invalidated.py b/tests/libcore/unittests/oas_model/test_can_be_invalidated.py similarity index 98% rename from tests/libcore/unittests/test_can_be_invalidated.py rename to tests/libcore/unittests/oas_model/test_can_be_invalidated.py index d17eafe..cde05b3 100644 --- a/tests/libcore/unittests/test_can_be_invalidated.py +++ b/tests/libcore/unittests/oas_model/test_can_be_invalidated.py @@ -1,7 +1,7 @@ # pylint: disable="missing-class-docstring", "missing-function-docstring" import unittest -from OpenApiLibCore.models import ( +from OpenApiLibCore.models.oas_models import ( ArraySchema, BooleanSchema, IntegerSchema, diff --git a/tests/libcore/unittests/oas_model/test_get_invalid_data.py b/tests/libcore/unittests/oas_model/test_get_invalid_data.py new file mode 100644 index 0000000..05b8fbd --- /dev/null +++ b/tests/libcore/unittests/oas_model/test_get_invalid_data.py @@ -0,0 +1,74 @@ +# pylint: disable="missing-class-docstring", "missing-function-docstring" +# pyright: reportArgumentType=false +import unittest + +from OpenApiLibCore import PropertyValueConstraint, RelationsMapping, ResourceRelation +from OpenApiLibCore.models.oas_models import ArraySchema, IntegerSchema + + +class ArrayConstraint(RelationsMapping): + @staticmethod + def get_relations() -> list[ResourceRelation]: + relations: list[ResourceRelation] = [ + PropertyValueConstraint( + property_name="something", + values=[24, 42], + invalid_value=33, + invalid_value_error_code=422, + ), + ] + return relations + + +class TestArraySchema(unittest.TestCase): + def test_raises_for_no_matching_status_code(self) -> None: + schema = ArraySchema(items=IntegerSchema()) + schema.attach_relations_mapping(ArrayConstraint) + with self.assertRaises(ValueError) as context: + _ = schema.get_invalid_data( + valid_data=[42], + status_code=500, + invalid_property_default_code=422, + ) + self.assertEqual( + str(context.exception), + "No constraint can be broken to cause status_code 500", + ) + + def test_status_code_is_default_code_without_constraints_raises(self) -> None: + schema = ArraySchema(items=IntegerSchema(maximum=43)) + schema.attach_relations_mapping(RelationsMapping) + with self.assertRaises(ValueError) as context: + _ = schema.get_invalid_data( + valid_data=[42], + status_code=422, + invalid_property_default_code=422, + ) + self.assertEqual( + str(context.exception), + "No constraint can be broken to cause status_code 422", + ) + + def test_status_code_is_default_code(self) -> None: + schema = ArraySchema(items=IntegerSchema(maximum=43), minItems=1) + schema.attach_relations_mapping(RelationsMapping) + invalid_data = schema.get_invalid_data( + valid_data=[42], + status_code=422, + invalid_property_default_code=422, + ) + self.assertEqual(invalid_data, []) + + valid_value = [42] + schema = ArraySchema(items=IntegerSchema(maximum=43), const=valid_value) + schema.attach_relations_mapping(RelationsMapping) + invalid_data = schema.get_invalid_data( + valid_data=valid_value, + status_code=422, + invalid_property_default_code=422, + ) + self.assertNotEqual(invalid_data, valid_value) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/libcore/unittests/oas_model/test_get_invalid_value.py b/tests/libcore/unittests/oas_model/test_get_invalid_value.py new file mode 100644 index 0000000..5a5d9e9 --- /dev/null +++ b/tests/libcore/unittests/oas_model/test_get_invalid_value.py @@ -0,0 +1,39 @@ +# pylint: disable="missing-class-docstring", "missing-function-docstring" +import unittest + +from OpenApiLibCore.models.oas_models import ( + BooleanSchema, + IntegerSchema, + StringSchema, +) + + +class TestGetInvalidValue(unittest.TestCase): + def test_value_error_handling(self) -> None: + values_from_constraints = [True, False] + schema = BooleanSchema() + invalid_value = schema.get_invalid_value( + valid_value=True, values_from_constraint=values_from_constraints + ) + self.assertIsInstance(invalid_value, str) + + def test_out_of_bounds(self) -> None: + schema = StringSchema(maxLength=3) + invalid_value = schema.get_invalid_value(valid_value="x") + self.assertIsInstance(invalid_value, (str, list)) + if isinstance(invalid_value, str): + self.assertTrue(len(invalid_value) > 3) + else: + self.assertEqual( + invalid_value, [{"invalid": [None, False]}, "null", None, True] + ) + + schema = IntegerSchema(minimum=5) + invalid_value = schema.get_invalid_value(valid_value=7) + self.assertIsInstance(invalid_value, (int, str)) + if isinstance(invalid_value, int): + self.assertTrue(invalid_value < 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/libcore/unittests/oas_model/test_get_request_data.py b/tests/libcore/unittests/oas_model/test_get_request_data.py new file mode 100644 index 0000000..90a8f2d --- /dev/null +++ b/tests/libcore/unittests/oas_model/test_get_request_data.py @@ -0,0 +1,190 @@ +# pylint: disable="missing-class-docstring", "missing-function-docstring" +import json +import pathlib +import unittest +from functools import partial + +from OpenApiLibCore.data_generation.data_generation_core import get_request_data +from OpenApiLibCore.models.oas_models import ( + ArraySchema, + BooleanSchema, + IntegerSchema, + NullSchema, + NumberSchema, + ObjectSchema, + OpenApiObject, + StringSchema, + UnionTypeSchema, +) + +unittest_folder = pathlib.Path(__file__).parent.resolve() +spec_path_3_0 = ( + unittest_folder.parent.parent.parent / "files" / "request_data_variations_3.0.json" +) +spec_path_3_1 = ( + unittest_folder.parent.parent.parent / "files" / "request_data_variations_3.1.json" +) + + +class TestValidData30(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + with open(file=spec_path_3_0) as json_file: + spec_dict = json.load(json_file) + cls.spec = OpenApiObject.model_validate(spec_dict) + for path_item in cls.spec.paths.values(): + path_item.update_operation_parameters() + path_item.replace_nullable_with_union() + cls._get_request_data = staticmethod( + partial(get_request_data, method="POST", openapi_spec=cls.spec) + ) + + def test_bool_schema(self) -> None: + request_data = self._get_request_data(path="/boolean_schema") + self.assertIsInstance(request_data.valid_data, bool) + self.assertIsInstance(request_data.body_schema, BooleanSchema) + + def test_int_schema(self) -> None: + request_data = self._get_request_data(path="/integer_schema") + self.assertIsInstance(request_data.valid_data, int) + self.assertIsInstance(request_data.body_schema, IntegerSchema) + + def test_number_schema(self) -> None: + request_data = self._get_request_data(path="/number_schema") + self.assertIsInstance(request_data.valid_data, float) + self.assertIsInstance(request_data.body_schema, NumberSchema) + + def test_string_schema(self) -> None: + request_data = self._get_request_data(path="/string_schema") + self.assertIsInstance(request_data.valid_data, str) + self.assertIsInstance(request_data.body_schema, StringSchema) + + def test_string_schema_for_byte_format(self) -> None: + request_data = self._get_request_data(path="/bytes_schema") + self.assertIsInstance(request_data.valid_data, str) + self.assertIsInstance(request_data.body_schema, StringSchema) + + def test_object_schema(self) -> None: + request_data = self._get_request_data(path="/object_schema") + self.assertIsInstance(request_data.valid_data, dict) + self.assertIsInstance(request_data.body_schema, ObjectSchema) + + def test_array_schema(self) -> None: + request_data = self._get_request_data(path="/array_schema") + self.assertIsInstance(request_data.valid_data, list) + self.assertIsInstance(request_data.body_schema, ArraySchema) + self.assertIsInstance(request_data.body_schema.items, NumberSchema) + + def test_union_schema(self) -> None: + request_data = self._get_request_data(path="/union_schema") + self.assertIsInstance(request_data.valid_data, (type(None), int, str)) + self.assertTrue( + isinstance( + request_data.body_schema, (NullSchema, IntegerSchema, StringSchema) + ) + ) + + def test_array_with_union_schema(self) -> None: + request_data = self._get_request_data(path="/array_with_union_schema") + self.assertIsInstance(request_data.valid_data, list) + self.assertIsInstance(request_data.valid_data[0], (dict, type(None))) + self.assertTrue(isinstance(request_data.body_schema, ArraySchema)) + items_schema = request_data.body_schema.items + self.assertIsInstance(items_schema, UnionTypeSchema) + [object_schema] = items_schema.resolved_schemas + self.assertIsInstance(object_schema, ObjectSchema) + self.assertEqual(object_schema.required, ["name"]) + self.assertIsInstance(object_schema.additionalProperties, UnionTypeSchema) + additional_properties_schemas = ( + object_schema.additionalProperties.resolved_schemas + ) + self.assertIsInstance(additional_properties_schemas[0], BooleanSchema) + self.assertIsInstance( + additional_properties_schemas[1], (IntegerSchema, NumberSchema) + ) + + +class TestValidData31(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + with open(file=spec_path_3_1) as json_file: + spec_dict = json.load(json_file) + cls.spec = OpenApiObject.model_validate(spec_dict) + for path_item in cls.spec.paths.values(): + path_item.update_operation_parameters() + path_item.replace_nullable_with_union() + cls._get_request_data = staticmethod( + partial(get_request_data, method="POST", openapi_spec=cls.spec) + ) + + def test_null_schema(self) -> None: + request_data = self._get_request_data(path="/null_schema") + self.assertEqual(request_data.valid_data, None) + self.assertIsInstance(request_data.body_schema, NullSchema) + + def test_bool_schema(self) -> None: + request_data = self._get_request_data(path="/boolean_schema") + self.assertIsInstance(request_data.valid_data, bool) + self.assertIsInstance(request_data.body_schema, BooleanSchema) + + def test_int_schema(self) -> None: + request_data = self._get_request_data(path="/integer_schema") + self.assertIsInstance(request_data.valid_data, int) + self.assertIsInstance(request_data.body_schema, IntegerSchema) + + def test_number_schema(self) -> None: + request_data = self._get_request_data(path="/number_schema") + self.assertIsInstance(request_data.valid_data, float) + self.assertIsInstance(request_data.body_schema, NumberSchema) + + def test_string_schema(self) -> None: + request_data = self._get_request_data(path="/string_schema") + self.assertIsInstance(request_data.valid_data, str) + self.assertIsInstance(request_data.body_schema, StringSchema) + + def test_string_schema_for_byte_format(self) -> None: + request_data = self._get_request_data(path="/bytes_schema") + self.assertIsInstance(request_data.valid_data, str) + self.assertIsInstance(request_data.body_schema, StringSchema) + + def test_object_schema(self) -> None: + request_data = self._get_request_data(path="/object_schema") + self.assertIsInstance(request_data.valid_data, dict) + self.assertIsInstance(request_data.body_schema, ObjectSchema) + + def test_array_schema(self) -> None: + request_data = self._get_request_data(path="/array_schema") + self.assertIsInstance(request_data.valid_data, list) + self.assertIsInstance(request_data.body_schema, ArraySchema) + self.assertIsInstance(request_data.body_schema.items, NumberSchema) + + def test_union_schema(self) -> None: + request_data = self._get_request_data(path="/union_schema") + self.assertIsInstance(request_data.valid_data, (type(None), int, str)) + self.assertTrue( + isinstance( + request_data.body_schema, (NullSchema, IntegerSchema, StringSchema) + ) + ) + + def test_array_with_union_schema(self) -> None: + request_data = self._get_request_data(path="/array_with_union_schema") + self.assertIsInstance(request_data.valid_data, list) + self.assertIsInstance(request_data.valid_data[0], dict) + self.assertTrue(isinstance(request_data.body_schema, ArraySchema)) + items_schema = request_data.body_schema.items + self.assertIsInstance(items_schema, UnionTypeSchema) + [resolved_schema] = items_schema.resolved_schemas + self.assertEqual(resolved_schema.required, ["name"]) + self.assertIsInstance(resolved_schema.additionalProperties, UnionTypeSchema) + additional_properties_schemas = ( + resolved_schema.additionalProperties.resolved_schemas + ) + self.assertIsInstance(additional_properties_schemas[0], BooleanSchema) + self.assertIsInstance( + additional_properties_schemas[1], (IntegerSchema, NumberSchema) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/libcore/unittests/test_get_valid_value.py b/tests/libcore/unittests/oas_model/test_get_valid_value.py similarity index 69% rename from tests/libcore/unittests/test_get_valid_value.py rename to tests/libcore/unittests/oas_model/test_get_valid_value.py index 35c73bf..28a4297 100644 --- a/tests/libcore/unittests/test_get_valid_value.py +++ b/tests/libcore/unittests/oas_model/test_get_valid_value.py @@ -1,7 +1,7 @@ # pylint: disable="missing-class-docstring", "missing-function-docstring" import unittest -from OpenApiLibCore.models import ( +from OpenApiLibCore.models.oas_models import ( ArraySchema, BooleanSchema, IntegerSchema, @@ -16,142 +16,142 @@ class TestDefaults(unittest.TestCase): def test_null_schema(self) -> None: schema = NullSchema() - self.assertEqual(schema.get_valid_value(), None) + self.assertEqual(schema.get_valid_value()[0], None) def test_boolean_schema(self) -> None: schema = BooleanSchema() - self.assertIsInstance(schema.get_valid_value(), bool) + self.assertIsInstance(schema.get_valid_value()[0], bool) def test_string_schema(self) -> None: schema = StringSchema() - self.assertIsInstance(schema.get_valid_value(), str) + self.assertIsInstance(schema.get_valid_value()[0], str) def test_integer_schema(self) -> None: schema = IntegerSchema() - self.assertIsInstance(schema.get_valid_value(), int) + self.assertIsInstance(schema.get_valid_value()[0], int) def test_number_schema(self) -> None: schema = NumberSchema() - self.assertIsInstance(schema.get_valid_value(), float) + self.assertIsInstance(schema.get_valid_value()[0], float) def test_array_schema(self) -> None: schema = ArraySchema(items=IntegerSchema()) - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertIsInstance(value, list) - self.assertIsInstance(value[0], int) + if value: + self.assertIsInstance(value[0], int) def test_object_schema(self) -> None: schema = ObjectSchema() - with self.assertRaises(NotImplementedError): - schema.get_valid_value() + value = schema.get_valid_value()[0] + self.assertIsInstance(value, dict) def test_union_schema(self) -> None: schema = UnionTypeSchema(oneOf=[BooleanSchema(), IntegerSchema()]) - self.assertIsInstance(schema.get_valid_value(), int) + self.assertIsInstance(schema.get_valid_value()[0], int) class TestGetValidValueFromConst(unittest.TestCase): def test_boolean_schema(self) -> None: const = False schema = BooleanSchema(const=const) - self.assertEqual(schema.get_valid_value(), const) + self.assertEqual(schema.get_valid_value()[0], const) def test_string_schema(self) -> None: const = "Hello world!" schema = StringSchema(const=const) - self.assertEqual(schema.get_valid_value(), const) + self.assertEqual(schema.get_valid_value()[0], const) def test_integer_schema(self) -> None: const = 42 schema = IntegerSchema(const=const) - self.assertEqual(schema.get_valid_value(), const) + self.assertEqual(schema.get_valid_value()[0], const) def test_number_schema(self) -> None: const = 3.14 schema = NumberSchema(const=const) - self.assertEqual(schema.get_valid_value(), const) + self.assertEqual(schema.get_valid_value()[0], const) def test_array_schema(self) -> None: const = ["foo", "bar"] schema = ArraySchema(items=StringSchema(), const=const) - self.assertEqual(schema.get_valid_value(), const) + self.assertEqual(schema.get_valid_value()[0], const) def test_object_schema(self) -> None: const = {"foo": 42, "bar": 3.14} schema = ObjectSchema(const=const) - with self.assertRaises(NotImplementedError): - schema.get_valid_value() + self.assertEqual(schema.get_valid_value()[0], const) class TestGetValidValueFromEnum(unittest.TestCase): def test_string_schema(self) -> None: enum = ["eggs", "bacon", "spam"] schema = StringSchema(enum=enum) - self.assertIn(schema.get_valid_value(), enum) + self.assertIn(schema.get_valid_value()[0], enum) def test_integer_schema(self) -> None: enum = [1, 3, 5, 7] schema = IntegerSchema(enum=enum) - self.assertIn(schema.get_valid_value(), enum) + self.assertIn(schema.get_valid_value()[0], enum) def test_number_schema(self) -> None: enum = [0.1, 0.01, 0.001] schema = NumberSchema(enum=enum) - self.assertIn(schema.get_valid_value(), enum) + self.assertIn(schema.get_valid_value()[0], enum) def test_array_schema(self) -> None: enum = [["foo", "bar"], ["eggs", "bacon", "spam"]] schema = ArraySchema(items=StringSchema(), enum=enum) - self.assertIn(schema.get_valid_value(), enum) + self.assertIn(schema.get_valid_value()[0], enum) def test_object_schema(self) -> None: enum: list[dict[str, int | float]] = [{"foo": 42, "bar": 3.14}] schema = ObjectSchema(enum=enum) - with self.assertRaises(NotImplementedError): - schema.get_valid_value() + value = schema.get_valid_value()[0] + self.assertIn(value, enum) class TestStringSchemaVariations(unittest.TestCase): def test_default_min_max(self) -> None: schema = StringSchema(maxLength=0) - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertEqual(value, "") schema = StringSchema(minLength=36) - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertEqual(len(value), 36) def test_min_max(self) -> None: schema = StringSchema(minLength=42, maxLength=42) - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertEqual(len(value), 42) schema = StringSchema(minLength=42) - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertEqual(len(value), 42) def test_datetime(self) -> None: schema = StringSchema(format="date-time") - value = schema.get_valid_value() + value = schema.get_valid_value()[0] matcher = r"^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)((-(\d{2}):(\d{2})|Z)?)$" self.assertRegex(value, matcher) def test_date(self) -> None: schema = StringSchema(format="date") - value = schema.get_valid_value() + value = schema.get_valid_value()[0] matcher = r"^(\d{4})-(\d{2})-(\d{2})$" self.assertRegex(value, matcher) def test_pattern(self) -> None: pattern = r"^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[A-Za-z]{2}$" schema = StringSchema(pattern=pattern) - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertRegex(value, pattern) pattern = r"^(?:[\p{L}\p{Mn}\p{Nd}.,()'-]+(?:['.’ ]|\s?[&\/\p{Pd}]\s?)?)+[\p{L}\p{Mn}\p{Nd}]\.?$" schema = StringSchema(pattern=pattern) with self.assertLogs(level="WARN") as logs: - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertTrue(len(logs.output) > 0) last_log_entry = logs.output[-1] @@ -166,34 +166,29 @@ def test_pattern(self) -> None: last_log_entry.endswith(f"The pattern was: {pattern}"), last_log_entry ) - def test_byte(self) -> None: - schema = StringSchema(format="byte") - value = schema.get_valid_value() - self.assertIsInstance(value, bytes) - class TestArraySchemaVariations(unittest.TestCase): def test_default_min_max(self) -> None: schema = ArraySchema(items=StringSchema()) - value = schema.get_valid_value() - self.assertEqual(len(value), 1) + value = schema.get_valid_value()[0] + self.assertIn(len(value), (0, 1)) schema = {"maxItems": 0, "items": {"type": "string"}} schema = ArraySchema(items=StringSchema(), maxItems=0) - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertEqual(value, []) def test_min_max(self) -> None: - schema = ArraySchema(items=StringSchema(), maxItems=3) - value = schema.get_valid_value() - self.assertEqual(len(value), 3) + schema = ArraySchema(items=StringSchema(), maxItems=3, minItems=2) + value = schema.get_valid_value()[0] + self.assertIn(len(value), (2, 3)) schema = ArraySchema(items=StringSchema(), minItems=5) - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertEqual(len(value), 5) schema = ArraySchema(items=StringSchema(), minItems=7, maxItems=5) - value = schema.get_valid_value() + value = schema.get_valid_value()[0] self.assertEqual(len(value), 7) diff --git a/tests/libcore/unittests/test_get_values_out_of_bounds.py b/tests/libcore/unittests/oas_model/test_get_values_out_of_bounds.py similarity index 99% rename from tests/libcore/unittests/test_get_values_out_of_bounds.py rename to tests/libcore/unittests/oas_model/test_get_values_out_of_bounds.py index 6b16503..79a994f 100644 --- a/tests/libcore/unittests/test_get_values_out_of_bounds.py +++ b/tests/libcore/unittests/oas_model/test_get_values_out_of_bounds.py @@ -2,7 +2,7 @@ import unittest from sys import float_info -from OpenApiLibCore.models import ( +from OpenApiLibCore.models.oas_models import ( ArraySchema, BooleanSchema, IntegerSchema, diff --git a/tests/libcore/unittests/oas_model/test_handle_nullable.py b/tests/libcore/unittests/oas_model/test_handle_nullable.py new file mode 100644 index 0000000..029d578 --- /dev/null +++ b/tests/libcore/unittests/oas_model/test_handle_nullable.py @@ -0,0 +1,291 @@ +# pylint: disable="missing-class-docstring", "missing-function-docstring" +import json +import pathlib +import unittest +from functools import partial + +from OpenApiLibCore.data_generation.data_generation_core import get_request_data +from OpenApiLibCore.models.oas_models import ( + ArraySchema, + BooleanSchema, + IntegerSchema, + NullSchema, + NumberSchema, + ObjectSchema, + OpenApiObject, + SchemaObjectTypes, + StringSchema, + UnionTypeSchema, +) + +unittest_folder = pathlib.Path(__file__).parent.resolve() +spec_path = ( + unittest_folder.parent.parent.parent / "files" / "nullable_schema_variations.json" +) + + +class TestValidData30(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + with open(file=spec_path) as json_file: + spec_dict = json.load(json_file) + cls.spec = OpenApiObject.model_validate(spec_dict) + for path_item in cls.spec.paths.values(): + path_item.update_operation_parameters() + path_item.replace_nullable_with_union() + cls._get_request_data = staticmethod( + partial(get_request_data, method="POST", openapi_spec=cls.spec) + ) + + def get_body_schema_by_path(self, path: str) -> SchemaObjectTypes: + return ( + self.spec.paths[path].post.requestBody.content["application/json"].schema_ + ) + + def test_boolean_schema(self) -> None: + python_type = bool + path = "/boolean_schema" + schema_type = BooleanSchema + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (python_type, type(None))) + self.assertIsInstance(request_data.body_schema, (schema_type, NullSchema)) + if isinstance(request_data.body_schema, schema_type): + self.assertIsInstance(request_data.valid_data, python_type) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + bool_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(bool_schema, schema_type) + self.assertIsInstance(null_schema, NullSchema) + + def test_integer_schema(self) -> None: + python_type = int + path = "/integer_schema" + schema_type = IntegerSchema + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (python_type, type(None))) + self.assertIsInstance(request_data.body_schema, (schema_type, NullSchema)) + if isinstance(request_data.body_schema, schema_type): + self.assertIsInstance(request_data.valid_data, python_type) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + type_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(type_schema, schema_type) + self.assertIsInstance(null_schema, NullSchema) + + def test_number_schema(self) -> None: + python_type = float + path = "/number_schema" + schema_type = NumberSchema + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (python_type, type(None))) + self.assertIsInstance(request_data.body_schema, (schema_type, NullSchema)) + if isinstance(request_data.body_schema, schema_type): + self.assertIsInstance(request_data.valid_data, python_type) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + type_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(type_schema, schema_type) + self.assertIsInstance(null_schema, NullSchema) + + def test_string_schema(self) -> None: + python_type = str + path = "/string_schema" + schema_type = StringSchema + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (python_type, type(None))) + self.assertIsInstance(request_data.body_schema, (schema_type, NullSchema)) + if isinstance(request_data.body_schema, schema_type): + self.assertIsInstance(request_data.valid_data, python_type) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + type_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(type_schema, schema_type) + self.assertIsInstance(null_schema, NullSchema) + + def test_array_schema(self) -> None: + python_type = list + path = "/array_schema" + schema_type = ArraySchema + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (python_type, type(None))) + self.assertIsInstance(request_data.body_schema, (schema_type, NullSchema)) + if isinstance(request_data.body_schema, schema_type): + self.assertIsInstance(request_data.valid_data, python_type) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + type_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(type_schema, schema_type) + self.assertIsInstance(null_schema, NullSchema) + + def test_object_schema(self) -> None: + python_type = dict + path = "/object_schema" + schema_type = ObjectSchema + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (python_type, type(None))) + self.assertIsInstance(request_data.body_schema, (schema_type, NullSchema)) + if isinstance(request_data.body_schema, schema_type): + self.assertIsInstance(request_data.valid_data, python_type) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + type_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(type_schema, schema_type) + self.assertIsInstance(null_schema, NullSchema) + + def test_oneof_union_schema(self) -> None: + path = "/oneof_first" + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (int, str, type(None))) + self.assertIsInstance( + request_data.body_schema, (IntegerSchema, StringSchema, NullSchema) + ) + if isinstance(request_data.body_schema, (IntegerSchema, StringSchema)): + self.assertIsInstance(request_data.valid_data, (int, str)) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + integer_schema, string_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(integer_schema, IntegerSchema) + self.assertIsInstance(string_schema, StringSchema) + self.assertIsInstance(null_schema, NullSchema) + + path = "/oneof_second" + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (int, str, type(None))) + self.assertIsInstance( + request_data.body_schema, (IntegerSchema, StringSchema, NullSchema) + ) + if isinstance(request_data.body_schema, (IntegerSchema, StringSchema)): + self.assertIsInstance(request_data.valid_data, (int, str)) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + integer_schema, string_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(integer_schema, IntegerSchema) + self.assertIsInstance(string_schema, StringSchema) + self.assertIsInstance(null_schema, NullSchema) + + path = "/oneof_both" + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (int, str, type(None))) + self.assertIsInstance( + request_data.body_schema, (IntegerSchema, StringSchema, NullSchema) + ) + if isinstance(request_data.body_schema, (IntegerSchema, StringSchema)): + self.assertIsInstance(request_data.valid_data, (int, str)) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + integer_schema, string_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(integer_schema, IntegerSchema) + self.assertIsInstance(string_schema, StringSchema) + self.assertIsInstance(null_schema, NullSchema) + + def test_anyof_union_schema(self) -> None: + path = "/anyof_first" + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (int, str, type(None))) + self.assertIsInstance( + request_data.body_schema, (IntegerSchema, StringSchema, NullSchema) + ) + if isinstance(request_data.body_schema, (IntegerSchema, StringSchema)): + self.assertIsInstance(request_data.valid_data, (int, str)) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + integer_schema, string_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(integer_schema, IntegerSchema) + self.assertIsInstance(string_schema, StringSchema) + self.assertIsInstance(null_schema, NullSchema) + + path = "/anyof_second" + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (int, str, type(None))) + self.assertIsInstance( + request_data.body_schema, (IntegerSchema, StringSchema, NullSchema) + ) + if isinstance(request_data.body_schema, (IntegerSchema, StringSchema)): + self.assertIsInstance(request_data.valid_data, (int, str)) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + integer_schema, string_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(integer_schema, IntegerSchema) + self.assertIsInstance(string_schema, StringSchema) + self.assertIsInstance(null_schema, NullSchema) + + path = "/anyof_both" + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (int, str, type(None))) + self.assertIsInstance( + request_data.body_schema, (IntegerSchema, StringSchema, NullSchema) + ) + if isinstance(request_data.body_schema, (IntegerSchema, StringSchema)): + self.assertIsInstance(request_data.valid_data, (int, str)) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + integer_schema, string_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(integer_schema, IntegerSchema) + self.assertIsInstance(string_schema, StringSchema) + self.assertIsInstance(null_schema, NullSchema) + + def test_allof_union_schema(self) -> None: + path = "/allof_one" + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, dict) + self.assertIsInstance(request_data.body_schema, ObjectSchema) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + [object_schema] = body_schema.resolved_schemas + self.assertIsInstance(object_schema, ObjectSchema) + + path = "/allof_all" + request_data = self._get_request_data(path) + self.assertIsInstance(request_data.valid_data, (dict, type(None))) + self.assertIsInstance(request_data.body_schema, (ObjectSchema, NullSchema)) + if isinstance(request_data.body_schema, ObjectSchema): + self.assertIsInstance(request_data.valid_data, dict) + else: + self.assertEqual(request_data.valid_data, None) + + body_schema = self.get_body_schema_by_path(path) + self.assertIsInstance(body_schema, UnionTypeSchema) + object_schema, null_schema = body_schema.resolved_schemas + self.assertIsInstance(object_schema, ObjectSchema) + self.assertIsInstance(null_schema, NullSchema) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/libcore/unittests/test_invalid_value_from_const_or_enum.py b/tests/libcore/unittests/oas_model/test_invalid_value_from_const_or_enum.py similarity index 98% rename from tests/libcore/unittests/test_invalid_value_from_const_or_enum.py rename to tests/libcore/unittests/oas_model/test_invalid_value_from_const_or_enum.py index 79cd1de..7fdc814 100644 --- a/tests/libcore/unittests/test_invalid_value_from_const_or_enum.py +++ b/tests/libcore/unittests/oas_model/test_invalid_value_from_const_or_enum.py @@ -1,7 +1,7 @@ # pylint: disable="missing-class-docstring", "missing-function-docstring" import unittest -from OpenApiLibCore.models import ( +from OpenApiLibCore.models.oas_models import ( ArraySchema, BooleanSchema, IntegerSchema, diff --git a/tests/libcore/unittests/test_resolve_union_schema.py b/tests/libcore/unittests/oas_model/test_resolve_union_schema.py similarity index 89% rename from tests/libcore/unittests/test_resolve_union_schema.py rename to tests/libcore/unittests/oas_model/test_resolve_union_schema.py index 515c39c..a0fdbe6 100644 --- a/tests/libcore/unittests/test_resolve_union_schema.py +++ b/tests/libcore/unittests/oas_model/test_resolve_union_schema.py @@ -1,7 +1,7 @@ # pylint: disable="missing-class-docstring", "missing-function-docstring" import unittest -from OpenApiLibCore.models import ( +from OpenApiLibCore.models.oas_models import ( ArraySchema, BooleanSchema, IntegerSchema, @@ -16,27 +16,27 @@ class TestResolvedSchemasPropery(unittest.TestCase): def test_allof_only_supports_object_schemas(self) -> None: schema = UnionTypeSchema(allOf=[NullSchema()]) - with self.assertRaises(NotImplementedError): + with self.assertRaises(ValueError): schema.resolved_schemas schema = UnionTypeSchema(allOf=[BooleanSchema()]) - with self.assertRaises(NotImplementedError): + with self.assertRaises(ValueError): schema.resolved_schemas schema = UnionTypeSchema(allOf=[StringSchema()]) - with self.assertRaises(NotImplementedError): + with self.assertRaises(ValueError): schema.resolved_schemas schema = UnionTypeSchema(allOf=[IntegerSchema()]) - with self.assertRaises(NotImplementedError): + with self.assertRaises(ValueError): schema.resolved_schemas schema = UnionTypeSchema(allOf=[NumberSchema()]) - with self.assertRaises(NotImplementedError): + with self.assertRaises(ValueError): schema.resolved_schemas schema = UnionTypeSchema(allOf=[ArraySchema(items=StringSchema())]) - with self.assertRaises(NotImplementedError): + with self.assertRaises(ValueError): schema.resolved_schemas def test_allof_not_compatible_with_const(self) -> None: diff --git a/tests/libcore/unittests/test_dto_utils.py b/tests/libcore/unittests/test_dto_utils.py deleted file mode 100644 index db1df7a..0000000 --- a/tests/libcore/unittests/test_dto_utils.py +++ /dev/null @@ -1,79 +0,0 @@ -# pylint: disable="missing-class-docstring", "missing-function-docstring" -import pathlib -import sys -import unittest - -from OpenApiLibCore import ( - Dto, - IdDependency, - IdReference, - PathPropertiesConstraint, - PropertyValueConstraint, - UniquePropertyValueConstraint, - dto_utils, -) - -unittest_folder = pathlib.Path(__file__).parent.resolve() -mappings_path = ( - unittest_folder.parent.parent / "user_implemented" / "custom_user_mappings.py" -) - - -class TestDefaultDto(unittest.TestCase): - def test_can_init(self) -> None: - default_dto = dto_utils.DefaultDto() - self.assertIsInstance(default_dto, Dto) - - -class TestGetDtoClass(unittest.TestCase): - mappings_module_name = "" - - @classmethod - def setUpClass(cls) -> None: - if mappings_path.is_file(): - mappings_folder = str(mappings_path.parent) - sys.path.append(mappings_folder) - cls.mappings_module_name = mappings_path.stem - print(f"added {mappings_folder} to path") - else: - assert False, "The mappings_path is not a file." - - @classmethod - def tearDownClass(cls) -> None: - if mappings_path.is_file(): - print(f"removed {sys.path.pop()} from path") - - def test_no_mapping(self) -> None: - get_dto_class_instance = dto_utils.get_dto_class("dummy") - self.assertDictEqual(get_dto_class_instance.dto_mapping, {}) - - def test_valid_mapping(self) -> None: - get_dto_class_instance = dto_utils.get_dto_class(self.mappings_module_name) - self.assertIsInstance(get_dto_class_instance.dto_mapping, dict) - self.assertGreater(len(get_dto_class_instance.dto_mapping.keys()), 0) - - def mapped_returns_dto_instance(self) -> None: - get_dto_class_instance = dto_utils.get_dto_class(self.mappings_module_name) - keys = get_dto_class_instance.dto_mapping.keys() - for key in keys: - self.assertIsInstance(key, tuple) - self.assertEqual(len(key), 2) - self.assertIsInstance( - get_dto_class_instance(key), - ( - IdDependency, - IdReference, - PropertyValueConstraint, - UniquePropertyValueConstraint, - ), - ) - - def unmapped_returns_defaultdto(self) -> None: - get_dto_class_instance = dto_utils.get_dto_class(self.mappings_module_name) - self.assertIsInstance( - get_dto_class_instance(("dummy", "post")), dto_utils.DefaultDto - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/libcore/unittests/test_localized_faker.py b/tests/libcore/unittests/test_localized_faker.py index 54af991..146b1ba 100644 --- a/tests/libcore/unittests/test_localized_faker.py +++ b/tests/libcore/unittests/test_localized_faker.py @@ -2,7 +2,7 @@ import datetime import unittest -from OpenApiLibCore.localized_faker import LocalizedFaker +from OpenApiLibCore.data_generation.localized_faker import LocalizedFaker class TestLocalizedFaker(unittest.TestCase): diff --git a/tests/libcore/unittests/test_mappings.py b/tests/libcore/unittests/test_mappings.py new file mode 100644 index 0000000..ffe9f3b --- /dev/null +++ b/tests/libcore/unittests/test_mappings.py @@ -0,0 +1,130 @@ +# pylint: disable="missing-class-docstring", "missing-function-docstring" +import pathlib +import sys +import unittest + +from OpenApiLibCore import RelationsMapping +from OpenApiLibCore.data_relations.relations_base import ( + GetIdPropertyName, + get_id_property_name, + get_path_mapping_dict, + get_relations_mapping_dict, +) +from OpenApiLibCore.utils.id_mapping import dummy_transformer + +unittest_folder = pathlib.Path(__file__).parent.resolve() +mappings_path = ( + unittest_folder.parent.parent / "user_implemented" / "custom_user_mappings.py" +) + + +class TestRelationsMapping(unittest.TestCase): + mappings_module_name = "" + + @classmethod + def setUpClass(cls) -> None: + if mappings_path.is_file(): + mappings_folder = str(mappings_path.parent) + sys.path.append(mappings_folder) + cls.mappings_module_name = mappings_path.stem + print(f"added {mappings_folder} to path") + else: + assert False, "The mappings_path is not a file." + + @classmethod + def tearDownClass(cls) -> None: + if mappings_path.is_file(): + print(f"removed {sys.path.pop()} from path") + + def test_no_mapping(self) -> None: + value_relations_mapping_dict = get_relations_mapping_dict("dummy") + self.assertDictEqual(value_relations_mapping_dict, {}) + + def test_valid_mapping(self) -> None: + value_relations_mapping_dict = get_relations_mapping_dict( + self.mappings_module_name + ) + self.assertIsInstance(value_relations_mapping_dict, dict) + self.assertGreater(len(value_relations_mapping_dict.keys()), 0) + + def test_mapped_returns_relationsmapping_class(self) -> None: + value_relations_mapping_dict = get_relations_mapping_dict( + self.mappings_module_name + ) + keys = value_relations_mapping_dict.keys() + for key in keys: + self.assertIsInstance(key, tuple) + self.assertEqual(len(key), 2) + self.assertTrue( + issubclass(value_relations_mapping_dict[key], RelationsMapping) + ) + + +class TestPathMapping(unittest.TestCase): + mappings_module_name = "" + + @classmethod + def setUpClass(cls) -> None: + if mappings_path.is_file(): + mappings_folder = str(mappings_path.parent) + sys.path.append(mappings_folder) + cls.mappings_module_name = mappings_path.stem + print(f"added {mappings_folder} to path") + else: + assert False, "The mappings_path is not a file." + + def test_no_mapping(self) -> None: + path_mapping_dict = get_path_mapping_dict("dummy") + self.assertDictEqual(path_mapping_dict, {}) + + def test_valid_mapping(self) -> None: + path_mapping_dict = get_path_mapping_dict(self.mappings_module_name) + self.assertIsInstance(path_mapping_dict, dict) + self.assertGreater(len(path_mapping_dict.keys()), 0) + + +class TestIdPropertyNameMapping(unittest.TestCase): + mappings_module_name = "" + + @classmethod + def setUpClass(cls) -> None: + if mappings_path.is_file(): + mappings_folder = str(mappings_path.parent) + sys.path.append(mappings_folder) + cls.mappings_module_name = mappings_path.stem + print(f"added {mappings_folder} to path") + else: + assert False, "The mappings_path is not a file." + + def test_no_mapping(self) -> None: + id_property_name_mapping = get_id_property_name("dummy", "identifier") + self.assertIsInstance(id_property_name_mapping, GetIdPropertyName) + self.assertEqual( + id_property_name_mapping.default_id_property_name, "identifier" + ) + self.assertDictEqual(id_property_name_mapping.id_mapping, {}) + + def test_valid_mapping(self) -> None: + id_property_name_mapping = get_id_property_name( + self.mappings_module_name, + "id", + ) + self.assertIsInstance(id_property_name_mapping, GetIdPropertyName) + self.assertEqual(id_property_name_mapping.default_id_property_name, "id") + self.assertIsInstance(id_property_name_mapping.id_mapping, dict) + + not_mapped = id_property_name_mapping("/secret_message") + self.assertEqual(not_mapped[0], "id") + self.assertEqual(not_mapped[1], dummy_transformer) + + default_transformer = id_property_name_mapping("/wagegroups") + self.assertEqual(default_transformer[0], "wagegroup_id") + self.assertEqual(default_transformer[1], dummy_transformer) + + custom_transformer = id_property_name_mapping("/wagegroups/{wagegroup_id}") + self.assertEqual(custom_transformer[0], "wagegroup_id") + self.assertEqual(custom_transformer[1].__name__, "my_transformer") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/libcore/unittests/test_parameter_utils.py b/tests/libcore/unittests/test_parameter_utils.py index 2d5be9a..b857d57 100644 --- a/tests/libcore/unittests/test_parameter_utils.py +++ b/tests/libcore/unittests/test_parameter_utils.py @@ -1,7 +1,7 @@ # pylint: disable="missing-class-docstring", "missing-function-docstring" import unittest -from OpenApiLibCore.parameter_utils import ( +from OpenApiLibCore.utils.parameter_utils import ( get_oas_name_from_safe_name, get_safe_name_for_oas_name, ) diff --git a/tests/libcore/unittests/value_utils/test_invalid_value_from_constraint.py b/tests/libcore/unittests/value_utils/test_invalid_value_from_constraint.py index 3505ed5..f7718e2 100644 --- a/tests/libcore/unittests/value_utils/test_invalid_value_from_constraint.py +++ b/tests/libcore/unittests/value_utils/test_invalid_value_from_constraint.py @@ -1,147 +1,146 @@ # pylint: disable="missing-class-docstring", "missing-function-docstring" import unittest -from typing import Any -from OpenApiLibCore import IGNORE, value_utils +from OpenApiLibCore.models.oas_models import ( + ArraySchema, + BooleanSchema, + IntegerSchema, + NullSchema, + NumberSchema, + ObjectSchema, + StringSchema, + UnionTypeSchema, +) class TestInvalidValueFromConstraint(unittest.TestCase): - def test_ignore(self) -> None: - values = [42, IGNORE] - value = value_utils.get_invalid_value_from_constraint( - values_from_constraint=values, - value_type="irrelevant", - ) - self.assertEqual(value, IGNORE) - - def test_unsupported(self) -> None: - values = [{"red": 255, "green": 255, "blue": 255}] - with self.assertRaises(ValueError): - _ = value_utils.get_invalid_value_from_constraint( - values_from_constraint=values, - value_type="dummy", - ) - def test_bool(self) -> None: + schema = BooleanSchema() values = [True] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="boolean", ) self.assertNotIn(value, values) self.assertIsInstance(value, bool) values = [False] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="boolean", ) self.assertNotIn(value, values) self.assertIsInstance(value, bool) values = [True, False] with self.assertRaises(ValueError): - _ = value_utils.get_invalid_value_from_constraint( + _ = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="boolean", ) def test_string(self) -> None: + schema = StringSchema() values = ["foo"] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="string", ) self.assertNotIn(value, values) self.assertIsInstance(value, str) values = ["foo", "bar", "baz"] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="string", ) self.assertNotIn(value, values) self.assertIsInstance(value, str) values = [""] with self.assertRaises(ValueError): - _ = value_utils.get_invalid_value_from_constraint( + _ = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="string", ) def test_integer(self) -> None: + schema = IntegerSchema() values = [0] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="integer", ) self.assertNotIn(value, values) self.assertIsInstance(value, int) values = [-3, 0, 3] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="integer", ) self.assertNotIn(value, values) self.assertIsInstance(value, int) def test_number(self) -> None: + schema = NumberSchema() values = [0.0] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="number", ) self.assertNotIn(value, values) self.assertIsInstance(value, float) values = [-0.1, 0.0, 0.1] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="number", ) self.assertNotIn(value, values) self.assertIsInstance(value, float) def test_array(self) -> None: - values: list[Any] = [[42]] - value = value_utils.get_invalid_value_from_constraint( + schema = ArraySchema(items=IntegerSchema()) + values = [[42]] + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="array", ) self.assertNotIn(value, values) + for item in value: + self.assertIsInstance(item, int) + schema = ArraySchema(items=StringSchema()) values = [["spam"], ["ham", "eggs"]] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="array", ) self.assertNotIn(value, values) + for item in value: + self.assertIsInstance(item, str) - values = [] - with self.assertRaises(ValueError): - _ = value_utils.get_invalid_value_from_constraint( - values_from_constraint=values, - value_type="array", - ) - + schema = ArraySchema(items=ArraySchema(items=StringSchema())) values = [[], []] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="array", ) self.assertEqual(value, []) def test_object(self) -> None: + schema = ObjectSchema() values = [{"red": 255, "green": 255, "blue": 255}] - value = value_utils.get_invalid_value_from_constraint( + value = schema.get_invalid_value_from_constraint( values_from_constraint=values, - value_type="object", ) self.assertNotEqual(value, values[0]) self.assertIsInstance(value, dict) + def test_union(self) -> None: + schema = UnionTypeSchema() + values = [None] + with self.assertRaises(ValueError): + _ = schema.get_invalid_value_from_constraint( + values_from_constraint=values, + ) + + def test_null(self) -> None: + schema = NullSchema() + values = [None] + with self.assertRaises(ValueError): + _ = schema.get_invalid_value_from_constraint( + values_from_constraint=values, + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/libcore/unittests/value_utils/test_type_name_mappers.py b/tests/libcore/unittests/value_utils/test_type_name_mappers.py index d95244b..5c2a1a0 100644 --- a/tests/libcore/unittests/value_utils/test_type_name_mappers.py +++ b/tests/libcore/unittests/value_utils/test_type_name_mappers.py @@ -1,7 +1,7 @@ # pylint: disable="missing-class-docstring", "missing-function-docstring" import unittest -from OpenApiLibCore import value_utils +from OpenApiLibCore.data_generation import value_utils class TestTypeNameMappers(unittest.TestCase): diff --git a/tests/libgen/suites/test_multiple_libraries.robot b/tests/libgen/suites/test_multiple_libraries.robot new file mode 100644 index 0000000..2764142 --- /dev/null +++ b/tests/libgen/suites/test_multiple_libraries.robot @@ -0,0 +1,53 @@ +*** Settings *** +Library MyGeneratedLibrary +... source=${ORIGIN}/openapi.json +... origin=${ORIGIN} +... base_path=${EMPTY} +... mappings_path=${ROOT}/tests/user_implemented/custom_user_mappings.py +Library MyGeneratedLibrary +... source=${ORIGIN}/openapi.json +... origin=${ORIGIN} +... base_path=${EMPTY} +... mappings_path=${ROOT}/tests/user_implemented/custom_user_mappings.py +... AS MyDuplicate +Library MyOtherGeneratedLibrary +... source=${ORIGIN}/openapi.json +... origin=${ORIGIN} +... base_path=${EMPTY} +... mappings_path=${ROOT}/tests/user_implemented/custom_user_mappings.py +Library MyGeneratedEdgeCaseLibrary +... source=${ROOT}/tests/files/schema_with_parameter_name_duplication.yaml +... origin=${ORIGIN} +... AS EdgeCases + +Test Tags rf7 + + +*** Variables *** +${ORIGIN} http://localhost:8000 + + +*** Test Cases *** +Test Libraries From Same Spec + ${first_url}= MyGeneratedLibrary.Get Valid Url path=/employees + ${second_url}= MyOtherGeneratedLibrary.Get Valid Url path=/employees + Should Be Equal As Strings ${first_url} ${second_url} + + ${first_values}= MyGeneratedLibrary.Get Request Values path=/employees method=POST + VAR ${first_json}= ${first_values.json_data} + ${second_values}= MyDuplicate.Get Request Values path=/employees method=POST + VAR ${second_json}= ${second_values.json_data} + ${third_values}= MyOtherGeneratedLibrary.Get Request Values path=/employees method=POST + VAR ${third_json}= ${third_values.json_data} + Should Be Equal ${first_json.keys()} ${second_json.keys()} + Should Be Equal ${second_json.keys()} ${third_json.keys()} + + Get Employees + Run Keyword And Expect Error Multiple keywords with name 'Get Employees Employees Get' found. * + ... Get Employees Employees Get + MyGeneratedLibrary.Get Employees Employees Get + MyDuplicate.Get Employees Employees Get + +Test Import Alias + Run Keyword And Expect Error Failed to get a valid id using GET on http://localhost:8000/hypermedia + ... Get Hypermedia Name diff --git a/tests/server/testserver.py b/tests/server/testserver.py index 30690f6..2566248 100644 --- a/tests/server/testserver.py +++ b/tests/server/testserver.py @@ -177,6 +177,15 @@ def post_event(event: Event, draft: bool = Header(False)) -> Event: return event +@app.put("/events/", status_code=201, response_model=list[Event]) +def put_events(events: list[Event]) -> list[Event]: + for event in events: + event.details.append(Detail(detail=f"Published {datetime.datetime.now()}")) + event.details.append(Detail(detail="Event details subject to change.")) + EVENTS.append(event) + return events + + @app.get( "/energy_label/{zipcode}/{home_number}", status_code=200, @@ -292,7 +301,7 @@ def get_employees_in_wagegroup(wagegroup_id: str) -> list[EmployeeDetails]: "/employees", status_code=201, response_model=EmployeeDetails, - responses={403: {"model": Detail}, 451: {"model": Detail}}, + responses={400: {"model": Detail}, 403: {"model": Detail}, 451: {"model": Detail}}, ) def post_employee(employee: Employee, response: Response) -> EmployeeDetails: wagegroup_id = employee.wagegroup_id @@ -307,8 +316,9 @@ def post_employee(employee: Employee, response: Response) -> EmployeeDetails: status_code=403, detail="An employee must be at least 18 years old." ) parttime_schedule = employee.parttime_schedule - if parttime_schedule is not None: - parttime_schedule = ParttimeSchedule.model_validate(parttime_schedule) + if parttime_schedule is None: + raise HTTPException(status_code=400, detail="Data error.") + parttime_schedule = ParttimeSchedule.model_validate(parttime_schedule) new_employee = EmployeeDetails( identification=uuid4().hex, name=employee.name, diff --git a/tests/user_implemented/EtagListener.py b/tests/user_implemented/EtagListener.py index 557601a..88b2880 100644 --- a/tests/user_implemented/EtagListener.py +++ b/tests/user_implemented/EtagListener.py @@ -33,5 +33,8 @@ def start_keyword(self, data: KeywordData, result: KeywordResult) -> None: return get_result = run_keyword("authorized_request", url, "GET", params, headers) - etag = get_result.headers.get("etag") + lower_case_headers = { + key.lower(): value for key, value in get_result.headers.items() + } + etag = lower_case_headers.get("etag") result.args[3]["If-Match"] = etag diff --git a/tests/user_implemented/custom_user_mappings.py b/tests/user_implemented/custom_user_mappings.py index 30bc78a..ef56606 100644 --- a/tests/user_implemented/custom_user_mappings.py +++ b/tests/user_implemented/custom_user_mappings.py @@ -1,19 +1,19 @@ # pylint: disable=invalid-name -from typing import Type +from typing import Callable from OpenApiLibCore import ( IGNORE, - Dto, IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint, + RelationsMapping, ResourceRelation, UniquePropertyValueConstraint, ) -class WagegroupDto(Dto): +class WagegroupMapping(RelationsMapping): @staticmethod def get_relations() -> list[ResourceRelation]: relations: list[ResourceRelation] = [ @@ -42,7 +42,7 @@ def get_relations() -> list[ResourceRelation]: return relations -class WagegroupDeleteDto(Dto): +class WagegroupDeleteMapping(RelationsMapping): @staticmethod def get_relations() -> list[ResourceRelation]: relations: list[ResourceRelation] = [ @@ -60,7 +60,7 @@ def get_relations() -> list[ResourceRelation]: return relations -class ParttimeDayDto(Dto): +class ParttimeDayMapping(RelationsMapping): @staticmethod def get_relations() -> list[ResourceRelation]: relations: list[ResourceRelation] = [ @@ -72,19 +72,19 @@ def get_relations() -> list[ResourceRelation]: return relations -class ParttimeScheduleDto(Dto): +class ParttimeScheduleMapping(RelationsMapping): @staticmethod def get_relations() -> list[ResourceRelation]: relations: list[ResourceRelation] = [ PropertyValueConstraint( property_name="parttime_days", - values=[ParttimeDayDto], + values=[ParttimeDayMapping], ), ] return relations -class EmployeeDto(Dto): +class EmployeeMapping(RelationsMapping): @staticmethod def get_relations() -> list[ResourceRelation]: relations: list[ResourceRelation] = [ @@ -102,14 +102,16 @@ def get_relations() -> list[ResourceRelation]: ), PropertyValueConstraint( property_name="parttime_schedule", - values=[ParttimeScheduleDto], + values=[ParttimeScheduleMapping], treat_as_mandatory=True, + invalid_value=IGNORE, + invalid_value_error_code=400, ), ] return relations -class PatchEmployeeDto(EmployeeDto): +class PatchEmployeeMapping(RelationsMapping): @staticmethod def get_parameter_relations() -> list[ResourceRelation]: relations: list[ResourceRelation] = [ @@ -128,8 +130,26 @@ def get_parameter_relations() -> list[ResourceRelation]: ] return relations + @staticmethod + def get_relations() -> list[ResourceRelation]: + relations: list[ResourceRelation] = [ + IdDependency( + property_name="wagegroup_id", + get_path="/wagegroups", + error_code=451, + ), + PropertyValueConstraint( + property_name="date_of_birth", + values=["1970-07-07", "1980-08-08", "1990-09-09"], + invalid_value="2020-02-20", + invalid_value_error_code=403, + error_code=422, + ), + ] + return relations -class EnergyLabelDto(Dto): + +class EnergyLabelMapping(RelationsMapping): @staticmethod def get_path_relations() -> list[PathPropertiesConstraint]: relations: list[PathPropertiesConstraint] = [ @@ -153,7 +173,7 @@ def get_parameter_relations() -> list[ResourceRelation]: return relations -class MessageDto(Dto): +class MessageMapping(RelationsMapping): @staticmethod def get_parameter_relations() -> list[ResourceRelation]: relations: list[ResourceRelation] = [ @@ -176,26 +196,32 @@ def get_parameter_relations() -> list[ResourceRelation]: return relations -DTO_MAPPING: dict[tuple[str, str], Type[Dto]] = { - ("/wagegroups", "post"): WagegroupDto, - ("/wagegroups/{wagegroup_id}", "delete"): WagegroupDeleteDto, - ("/wagegroups/{wagegroup_id}", "put"): WagegroupDto, - ("/employees", "post"): EmployeeDto, - ("/employees/{employee_id}", "patch"): PatchEmployeeDto, - ("/energy_label/{zipcode}/{home_number}", "get"): EnergyLabelDto, - ("/secret_message", "get"): MessageDto, +RELATIONS_MAPPING: dict[tuple[str, str], type[RelationsMapping]] = { + ("/wagegroups", "post"): WagegroupMapping, + ("/wagegroups/{wagegroup_id}", "delete"): WagegroupDeleteMapping, + ("/wagegroups/{wagegroup_id}", "put"): WagegroupMapping, + ("/employees", "post"): EmployeeMapping, + ("/employees/{employee_id}", "patch"): PatchEmployeeMapping, + ("/energy_label/{zipcode}/{home_number}", "get"): EnergyLabelMapping, + ("/secret_message", "get"): MessageMapping, } + +def my_transformer(identifier_name: str) -> str: + return identifier_name.replace("/", "_") + + # NOTE: "/available_employees": "identification" is not mapped for testing purposes -ID_MAPPING: dict[str, str] = { +ID_MAPPING: dict[str, str | tuple[str, Callable[[str], str] | Callable[[int], int]]] = { "/employees": "identification", "/employees/{employee_id}": "identification", "/wagegroups": "wagegroup_id", - "/wagegroups/{wagegroup_id}": "wagegroup_id", + "/wagegroups/{wagegroup_id}": ("wagegroup_id", my_transformer), "/wagegroups/{wagegroup_id}/employees": "identification", } - -PATH_MAPPING: dict[str, Type[Dto]] = { - "/energy_label/{zipcode}/{home_number}": EnergyLabelDto, +# NOTE: WagegroupDeleteMapping does not have path mappings for testing purposes +PATH_MAPPING: dict[str, type[RelationsMapping]] = { + "/energy_label/{zipcode}/{home_number}": EnergyLabelMapping, + "/wagegroups/{wagegroup_id}": WagegroupDeleteMapping, } diff --git a/tests/variables.py b/tests/variables.py index cd2e373..be85c53 100644 --- a/tests/variables.py +++ b/tests/variables.py @@ -2,64 +2,7 @@ from requests.auth import HTTPDigestAuth -from OpenApiLibCore import ( - IGNORE, - DefaultDto, - Dto, - IdDependency, - IdReference, - PropertyValueConstraint, - ResourceRelation, - UniquePropertyValueConstraint, -) - - -class WagegroupDto(Dto): - @staticmethod - def get_relations() -> list[ResourceRelation]: - relations: list[ResourceRelation] = [ - UniquePropertyValueConstraint( - property_name="id", - value="Teapot", - error_code=418, - ), - IdReference( - property_name="wagegroup_id", - post_path="/employees", - error_code=406, - ), - PropertyValueConstraint( - property_name="overtime_percentage", - values=[IGNORE], - invalid_value=110, - invalid_value_error_code=422, - ), - PropertyValueConstraint( - property_name="hourly_rate", - values=[80.50, 90.95, 99.99], - ), - ] - return relations - - -class EmployeeDto(Dto): - @staticmethod - def get_relations() -> list[ResourceRelation]: - relations: list[ResourceRelation] = [ - IdDependency( - property_name="wagegroup_id", - get_path="/wagegroups", - error_code=451, - ), - PropertyValueConstraint( - property_name="date_of_birth", - values=["1970-07-07", "1980-08-08", "1990-09-09"], - invalid_value="2020-02-20", - invalid_value_error_code=403, - error_code=422, - ), - ] - return relations +from OpenApiLibCore import IdReference def get_variables() -> dict[str, Any]: @@ -74,16 +17,10 @@ def get_variables() -> dict[str, Any]: post_path="/employees/{employee_id}", error_code=406, ) - wagegroup_dto = WagegroupDto - employee_dto = EmployeeDto - default_dto = DefaultDto extra_headers: dict[str, str] = {"foo": "bar", "eggs": "bacon"} return { "ID_REFERENCE": id_reference, "INVALID_ID_REFERENCE": invalid_id_reference, - "DEFAULT_DTO": default_dto, - "WAGEGROUP_DTO": wagegroup_dto, - "EMPLOYEE_DTO": employee_dto, "EXTRA_HEADERS": extra_headers, "API_KEY": {"api_key": "Super secret key"}, "DIGEST_AUTH": HTTPDigestAuth(username="Jane", password="Joe"),