├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── description.md ├── generator.py ├── hint.md ├── input.md ├── output.md ├── problem.yml ├── samples │ ├── 0.in │ └── 0.out ├── solution.txt └── tests │ ├── 0.in │ ├── 0.out │ ├── 1.in │ └── 1.out └── src ├── generator_oj_problem ├── __init__.py ├── __main__.py ├── adapters │ ├── __init__.py │ ├── fps │ │ ├── __init__.py │ │ └── packer.py │ ├── generic │ │ ├── __init__.py │ │ ├── checker.py │ │ ├── initializer.py │ │ ├── loader.py │ │ └── paths.py │ └── hustoj │ │ └── __init__.py ├── generators │ ├── __init__.py │ ├── processors.py │ ├── template.py │ └── trimmers.py ├── models.py └── pipelines.py ├── pyproject.toml ├── requirements.txt └── setup.cfg /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/src" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 1 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | open-pull-requests-limit: 1 -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [released] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | persist-credentials: false 17 | - uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.7" 20 | architecture: "x64" 21 | - name: Build 22 | env: 23 | PYTHONUTF8: 1 24 | run: | 25 | cd src 26 | cp ../README.md ./README.md 27 | python -m pip install --upgrade build twine 28 | python -m build -o ../dist 29 | - name: Upload package artifacts 30 | uses: actions/upload-artifact@v3 31 | with: 32 | name: package 33 | path: ./dist 34 | test: 35 | needs: build 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | os: [macos-latest, windows-latest, ubuntu-latest] 40 | python: ["3.7", "3.8", "3.9", "3.10"] 41 | experimental: [false] 42 | include: 43 | - os: ubuntu-latest 44 | python: "3.11.0-rc.2" 45 | experimental: true 46 | continue-on-error: ${{ matrix.experimental }} 47 | runs-on: ${{ matrix.os }} 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v2.3.4 51 | with: 52 | persist-credentials: false 53 | - uses: actions/setup-python@v2 54 | with: 55 | python-version: ${{ matrix.python }} 56 | architecture: "x64" 57 | - name: Download artifacts 58 | uses: actions/download-artifact@v3 59 | with: 60 | name: package 61 | path: ./dist 62 | - name: Install 63 | env: 64 | PYTHONUTF8: 1 65 | run: python -m pip install ./dist/generator_oj_problem-0.0.3-py3-none-any.whl 66 | - name: Help 67 | run: gop --help 68 | - name: Generate Sample 69 | env: 70 | PYTHONUTF8: 1 71 | run: | 72 | cd demo 73 | gop gen -s 1 -c 5 --sample 74 | - name: Generate Test 75 | env: 76 | PYTHONUTF8: 1 77 | run: | 78 | cd demo 79 | gop gen -s 2 -c 10 80 | - name: Check before trimming 81 | env: 82 | PYTHONUTF8: 1 83 | run: | 84 | cd demo 85 | gop check 86 | - name: Trim 87 | env: 88 | PYTHONUTF8: 1 89 | run: | 90 | cd demo 91 | gop trim 92 | - name: Check 93 | env: 94 | PYTHONUTF8: 1 95 | run: | 96 | cd demo 97 | gop check 98 | - name: Pack 99 | env: 100 | PYTHONUTF8: 1 101 | run: | 102 | cd demo 103 | gop -a fps pack 104 | - name: Upload artifacts 105 | uses: actions/upload-artifact@v3 106 | with: 107 | name: demo-dist-${{ matrix.os }}-${{ matrix.python }} 108 | path: ./demo/dist 109 | deploy: 110 | if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'push' || github.event_name == 'release' }} 111 | needs: test 112 | runs-on: ubuntu-latest 113 | steps: 114 | - name: Download package artifacts 115 | uses: actions/download-artifact@v3 116 | with: 117 | name: package 118 | path: ./dist 119 | - name: Deploy packages 120 | if: ${{ github.event_name == 'release' }} 121 | env: 122 | PYTHONUTF8: 1 123 | TWINE_USERNAME: "__token__" 124 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 125 | run: | 126 | python -m pip install --upgrade build twine 127 | python -m twine upload --skip-existing --repository pypi "dist/*" 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | /.vscode/ 5 | /resources/* 6 | /temp/* 7 | **/dist/* 8 | 9 | # User-specific files 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # DNX 49 | project.lock.json 50 | project.fragment.lock.json 51 | artifacts/ 52 | 53 | *_i.c 54 | *_p.c 55 | *_i.h 56 | *.ilk 57 | *.meta 58 | *.obj 59 | *.pch 60 | *.pdb 61 | *.pgc 62 | *.pgd 63 | *.rsp 64 | *.sbr 65 | *.tlb 66 | *.tli 67 | *.tlh 68 | *.tmp 69 | *.tmp_proj 70 | *.log 71 | *.vspscc 72 | *.vssscc 73 | .builds 74 | *.pidb 75 | *.svclog 76 | *.scc 77 | 78 | # Chutzpah Test files 79 | _Chutzpah* 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opendb 86 | *.opensdf 87 | *.sdf 88 | *.cachefile 89 | *.VC.db 90 | *.VC.VC.opendb 91 | 92 | # Visual Studio profiler 93 | *.psess 94 | *.vsp 95 | *.vspx 96 | *.sap 97 | 98 | # TFS 2012 Local Workspace 99 | $tf/ 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | *.DotSettings.user 108 | 109 | # JustCode is a .NET coding add-in 110 | .JustCode 111 | 112 | # TeamCity is a build add-in 113 | _TeamCity* 114 | 115 | # DotCover is a Code Coverage Tool 116 | *.dotCover 117 | 118 | # NCrunch 119 | _NCrunch_* 120 | .*crunch*.local.xml 121 | nCrunchTemp_* 122 | 123 | # MightyMoose 124 | *.mm.* 125 | AutoTest.Net/ 126 | 127 | # Web workbench (sass) 128 | .sass-cache/ 129 | 130 | # Installshield output folder 131 | [Ee]xpress/ 132 | 133 | # DocProject is a documentation generator add-in 134 | DocProject/buildhelp/ 135 | DocProject/Help/*.HxT 136 | DocProject/Help/*.HxC 137 | DocProject/Help/*.hhc 138 | DocProject/Help/*.hhk 139 | DocProject/Help/*.hhp 140 | DocProject/Help/Html2 141 | DocProject/Help/html 142 | 143 | # Click-Once directory 144 | publish/ 145 | 146 | # Publish Web Output 147 | *.[Pp]ublish.xml 148 | *.azurePubxml 149 | # TODO: Comment the next line if you want to checkin your web deploy settings 150 | # but database connection strings (with potential passwords) will be unencrypted 151 | #*.pubxml 152 | *.publishproj 153 | 154 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 155 | # checkin your Azure Web App publish settings, but sensitive information contained 156 | # in these scripts will be unencrypted 157 | PublishScripts/ 158 | 159 | # NuGet Packages 160 | *.nupkg 161 | # The packages folder can be ignored because of Package Restore 162 | **/packages/* 163 | # except build/, which is used as an MSBuild target. 164 | !**/packages/build/ 165 | # Uncomment if necessary however generally it will be regenerated when needed 166 | #!**/packages/repositories.config 167 | # NuGet v3's project.json files produces more ignoreable files 168 | *.nuget.props 169 | *.nuget.targets 170 | 171 | # Microsoft Azure Build Output 172 | csx/ 173 | *.build.csdef 174 | 175 | # Microsoft Azure Emulator 176 | ecf/ 177 | rcf/ 178 | 179 | # Windows Store app package directories and files 180 | AppPackages/ 181 | BundleArtifacts/ 182 | Package.StoreAssociation.xml 183 | _pkginfo.txt 184 | 185 | # Visual Studio cache files 186 | # files ending in .cache can be ignored 187 | *.[Cc]ache 188 | # but keep track of directories ending in .cache 189 | !*.[Cc]ache/ 190 | 191 | # Others 192 | ClientBin/ 193 | ~$* 194 | *~ 195 | *.dbmdl 196 | *.dbproj.schemaview 197 | *.jfm 198 | *.pfx 199 | *.publishsettings 200 | node_modules/ 201 | orleans.codegen.cs 202 | 203 | # Since there are multiple workflows, uncomment next line to ignore bower_components 204 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 205 | #bower_components/ 206 | 207 | # RIA/Silverlight projects 208 | Generated_Code/ 209 | 210 | # Backup & report files from converting an old project file 211 | # to a newer Visual Studio version. Backup files are not needed, 212 | # because we have git ;-) 213 | _UpgradeReport_Files/ 214 | Backup*/ 215 | UpgradeLog*.XML 216 | UpgradeLog*.htm 217 | 218 | # SQL Server files 219 | *.mdf 220 | *.ldf 221 | 222 | # Business Intelligence projects 223 | *.rdl.data 224 | *.bim.layout 225 | *.bim_*.settings 226 | 227 | # Microsoft Fakes 228 | FakesAssemblies/ 229 | 230 | # GhostDoc plugin setting file 231 | *.GhostDoc.xml 232 | 233 | # Node.js Tools for Visual Studio 234 | .ntvs_analysis.dat 235 | 236 | # Visual Studio 6 build log 237 | *.plg 238 | 239 | # Visual Studio 6 workspace options file 240 | *.opt 241 | 242 | # Visual Studio LightSwitch build output 243 | **/*.HTMLClient/GeneratedArtifacts 244 | **/*.DesktopClient/GeneratedArtifacts 245 | **/*.DesktopClient/ModelManifest.xml 246 | **/*.Server/GeneratedArtifacts 247 | **/*.Server/ModelManifest.xml 248 | _Pvt_Extensions 249 | 250 | # Paket dependency manager 251 | .paket/paket.exe 252 | paket-files/ 253 | 254 | # FAKE - F# Make 255 | .fake/ 256 | 257 | # JetBrains Rider 258 | .idea/ 259 | *.sln.iml 260 | 261 | # CodeRush 262 | .cr/ 263 | 264 | # Python Tools for Visual Studio (PTVS) 265 | __pycache__/ 266 | *.pyc 267 | iExpr.Core/Properties/PublishProfiles/FolderProfile.pubxml 268 | iExpr.Core/Properties/PublishProfiles/FolderProfile.pubxml 269 | /src/Plugins/reCLI.Plugins.Music/Programs 270 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Generator-OJ-Problem](https://socialify.git.ci/StardustDL/generator-oj-problem/image?description=1&font=Bitter&forks=1&issues=1&language=1&owner=1&pulls=1&stargazers=1&theme=Light) 2 | 3 | [![](https://github.com/StardustDL/generator-oj-problem/workflows/CI/badge.svg)](https://github.com/StardustDL/generator-oj-problem/actions) [![](https://img.shields.io/github/license/StardustDL/generator-oj-problem.svg)](https://github.com/StardustDL/generator-oj-problem/blob/master/LICENSE) [![](https://img.shields.io/pypi/v/generator-oj-problem)](https://pypi.org/project/generator-oj-problem/) [![Downloads](https://pepy.tech/badge/generator-oj-problem?style=flat)](https://pepy.tech/project/generator-oj-problem) ![](https://img.shields.io/pypi/implementation/generator-oj-problem.svg?logo=pypi) ![](https://img.shields.io/pypi/pyversions/generator-oj-problem.svg?logo=pypi) ![](https://img.shields.io/pypi/wheel/generator-oj-problem.svg?logo=pypi) ![](https://img.shields.io/badge/Linux-yes-success?logo=linux) ![](https://img.shields.io/badge/Windows-yes-success?logo=windows) ![](https://img.shields.io/badge/MacOS-yes-success?logo=apple) ![](https://img.shields.io/badge/BSD-yes-success?logo=freebsd) 4 | 5 | A command-line tool to generate Online-Judge problem. 6 | 7 | - Render problem descriptions in Markdown to HTML 8 | - Check problem descriptions and data, including missing fields, UTF-8 encoding, end-of-line CRLF/LF 9 | - Packing problem data in freeproblemset(hustoj) format 10 | - Mechanism for generating input and output test data 11 | - Easy to define adapters for other online-judge platform 12 | 13 | Have fun! If you have any suggestions or find any bugs, please tell me. 14 | 15 | ## Install 16 | 17 | ```sh 18 | pip install generator-oj-problem 19 | 20 | # or use pipx for a standalone python environment 21 | pip install pipx 22 | pipx ensurepath 23 | pipx install generator-oj-problem 24 | 25 | gop --help 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```sh 31 | # Initialize your problem 32 | gop init 33 | 34 | # Modify the files to write problem 35 | ls . 36 | 37 | # Generate 2 sample data from id 1 38 | gop gen -s 1 -c 2 --sample 39 | # Generate 5 test data from id 2 40 | gop gen -s 2 -c 5 41 | 42 | # Trim sample and test data 43 | gop trim 44 | 45 | # Check validaty 46 | gop check 47 | 48 | # Pack your problem in FreeProblemSet format 49 | gop -a fps pack 50 | ``` 51 | 52 | > If you meet some encoding errors, ensure your Python interpreter runs in UTF-8 mode, e.g. adding **PYTHONUTF8=1** to your environment variables. 53 | 54 | ## Directory Structure 55 | 56 | > Here is a demo problem [A + B Problem](https://github.com/StardustDL/generator-oj-problem/tree/master/demo). Details about [`problem.yml`](https://github.com/StardustDL/generator-oj-problem/tree/master/demo/problem.yml) and [`generator.py`](https://github.com/StardustDL/generator-oj-problem/tree/master/demo/generator.py) are given at the comments of the demo files. 57 | > 58 | > The file with extension `.md` means it supports plain [CommonMark](https://commonmark.org/) by built-in render. **Attention**: No LaTeX and embeded image supports. 59 | 60 | - `problem.yml` Problem metadata and configuration 61 | - `description.md` Description 62 | - `input.md` Description of input 63 | - `output.md` Description of output 64 | - `hint.md` Hint 65 | - `solution.txt` Solution source code 66 | - `generator.py` Generator for input or output data. 67 | - `samples/` Sample data 68 | - `samples/0.in` Input of sample 69 | - `samples/0.out` Output of sample 70 | - `tests/` Test data (same form to `samples/`) 71 | -------------------------------------------------------------------------------- /demo/description.md: -------------------------------------------------------------------------------- 1 | Calculate `a+b`. -------------------------------------------------------------------------------- /demo/generator.py: -------------------------------------------------------------------------------- 1 | # Import the data container from generator-oj-problem 2 | from generator_oj_problem.generators import data 3 | 4 | # Write your generator code 5 | from random import randint 6 | x, y = randint(1, 1000), randint(1, 1000) 7 | 8 | # Write to input data, just like `print(x, y, file=input)` 9 | data.In(x, y) 10 | 11 | # Write to output data, just like `print(x, y, file=output)` 12 | data.Out(x + y) 13 | 14 | # Always keep this invocation at the end of the generator 15 | # to submit your data. 16 | data.submit() 17 | -------------------------------------------------------------------------------- /demo/hint.md: -------------------------------------------------------------------------------- 1 | Standard program: 2 | 3 | ```cpp 4 | #include 5 | using namespace std; 6 | int main() 7 | { 8 | int a, b; 9 | cin >> a >> b; 10 | cout << a + b << endl; 11 | return 0; 12 | } 13 | ``` -------------------------------------------------------------------------------- /demo/input.md: -------------------------------------------------------------------------------- 1 | Two integer `a`, `b` (0<=a,b<=10). -------------------------------------------------------------------------------- /demo/output.md: -------------------------------------------------------------------------------- 1 | Output `a+b` -------------------------------------------------------------------------------- /demo/problem.yml: -------------------------------------------------------------------------------- 1 | # Problem name 2 | name: A + B Problem 3 | # Author or source 4 | author: '' 5 | # Memory limit in MB 6 | memory: 128.0 7 | # Time limit in seconds 8 | time: 1.0 9 | # Use CRLF (\r\n) as the end-of-line sequence, set to false to use LF (\n) 10 | crlf: false 11 | # Programming language used by 'solution.txt' 12 | solutionLanguage: C++ 13 | -------------------------------------------------------------------------------- /demo/samples/0.in: -------------------------------------------------------------------------------- 1 | 1 2 2 | -------------------------------------------------------------------------------- /demo/samples/0.out: -------------------------------------------------------------------------------- 1 | 3 2 | -------------------------------------------------------------------------------- /demo/solution.txt: -------------------------------------------------------------------------------- 1 | // Standard program 2 | #include 3 | using namespace std; 4 | int main() 5 | { 6 | int a, b; 7 | cin >> a >> b; 8 | cout << a + b << endl; 9 | return 0; 10 | } -------------------------------------------------------------------------------- /demo/tests/0.in: -------------------------------------------------------------------------------- 1 | 2 3 2 | -------------------------------------------------------------------------------- /demo/tests/0.out: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /demo/tests/1.in: -------------------------------------------------------------------------------- 1 | 20 22 2 | -------------------------------------------------------------------------------- /demo/tests/1.out: -------------------------------------------------------------------------------- 1 | 42 2 | -------------------------------------------------------------------------------- /src/generator_oj_problem/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | 4 | __version__ = "0.0.3" 5 | 6 | 7 | def getAppDirectory() -> pathlib.Path: 8 | return pathlib.Path(__file__).parent.resolve() 9 | 10 | 11 | def getWorkingDirectory() -> pathlib.Path: 12 | return pathlib.Path(os.getcwd()).resolve() 13 | -------------------------------------------------------------------------------- /src/generator_oj_problem/__main__.py: -------------------------------------------------------------------------------- 1 | import code 2 | import logging 3 | import os 4 | import pathlib 5 | import sys 6 | from typing import Iterable 7 | 8 | import click 9 | import yaml 10 | from click import BadArgumentUsage, BadOptionUsage, BadParameter 11 | from click.exceptions import ClickException 12 | 13 | from . import __version__ 14 | from .pipelines import Pipeline 15 | from .adapters import build as buildAdapter, all as allAdapters 16 | from .models import Issue, Severity 17 | 18 | 19 | class AliasedGroup(click.Group): 20 | def get_command(self, ctx, cmd_name): 21 | rv = click.Group.get_command(self, ctx, cmd_name) 22 | if rv is not None: 23 | return rv 24 | matches = [x for x in self.list_commands(ctx) 25 | if x.startswith(cmd_name)] 26 | if not matches: 27 | return None 28 | elif len(matches) == 1: 29 | return click.Group.get_command(self, ctx, matches[0]) 30 | ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") 31 | 32 | def resolve_command(self, ctx, args): 33 | # always return the full command name 34 | _, cmd, args = super().resolve_command(ctx, args) 35 | return cmd.name, cmd, args 36 | 37 | 38 | icons = { 39 | Severity.Info: "🔵", 40 | Severity.Error: "🔴", 41 | Severity.Warning: "🟡" 42 | } 43 | results = { 44 | Severity.Info: "✅ Done successfully.", 45 | Severity.Error: "❌ Failed to process.", 46 | Severity.Warning: "❕ Done, but with warnings." 47 | } 48 | 49 | 50 | def printIssues(issues: "Iterable[Issue]", final: bool = True) -> Severity: 51 | maxLevel = Severity.Info 52 | for item in issues: 53 | print(f"{icons[item.level]} {item.message}") 54 | maxLevel = max(maxLevel, item.level) 55 | if final: 56 | print("-" * 50) 57 | print(results[maxLevel]) 58 | return maxLevel 59 | 60 | 61 | adapters = list(allAdapters()) 62 | pipeline: Pipeline = buildAdapter("generic") 63 | 64 | 65 | @click.command(cls=AliasedGroup) 66 | @click.pass_context 67 | @click.version_option(__version__, package_name="aexpy", prog_name="aexpy", message="%(prog)s v%(version)s.") 68 | @click.option("-a", "--adapter", type=click.Choice(adapters, case_sensitive=False), default="generic", help="Adapter to use.") 69 | @click.option('-D', '--directory', type=click.Path(exists=True, file_okay=False, resolve_path=True, path_type=pathlib.Path), default=".", help="Path to working directory.") 70 | def main(ctx=None, adapter: str = "generic", directory: pathlib.Path = ".") -> None: 71 | """ 72 | Generator-OJ-Problem 73 | 74 | A command-line tool to generate Online-Judge problem. 75 | 76 | Repository: https://github.com/StardustDL/generator-oj-problem 77 | """ 78 | 79 | os.chdir(directory) 80 | 81 | global pipeline 82 | pipeline = buildAdapter(adapter) 83 | 84 | 85 | @main.command() 86 | @click.option("-s", "--start", default=0, help="Start case id.") 87 | @click.option("-c", "--count", default=10, help="The number of generated cases.") 88 | @click.option("--sample", is_flag=True, help="Generate sample cases.") 89 | @click.option("-r", "--rewrite", is_flag=True, help="Rewrite existed cases.") 90 | def generate(start: int = 0, count: int = 10, sample: bool = False, rewrite: bool = False): 91 | """Generate input or output data.""" 92 | 93 | if printIssues(pipeline.generate(start, count, sample, rewrite)) == Severity.Error: 94 | raise ClickException("Failed to generate.") 95 | 96 | 97 | @main.command() 98 | def trim(): 99 | """Trim problem data (for end-of-line LF or CRLF).""" 100 | 101 | if printIssues(pipeline.trim()) == Severity.Error: 102 | raise ClickException("Failed to trim.") 103 | 104 | 105 | @main.command() 106 | def pack(): 107 | """Pack the problem.""" 108 | 109 | if printIssues(pipeline.pack()) == Severity.Error: 110 | raise ClickException("Failed to pack.") 111 | 112 | 113 | @main.command() 114 | def check(): 115 | """Check validity of the problem.""" 116 | 117 | if printIssues(pipeline.check()) == Severity.Error: 118 | raise ClickException("Failed to check.") 119 | 120 | 121 | @main.command() 122 | def initialize(): 123 | """Initialize problem working directory.""" 124 | 125 | if printIssues(pipeline.initialize()) == Severity.Error: 126 | raise ClickException("Failed to initialize.") 127 | 128 | 129 | if __name__ == '__main__': 130 | main() 131 | -------------------------------------------------------------------------------- /src/generator_oj_problem/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import pkgutil 3 | from ..pipelines import Pipeline 4 | 5 | 6 | def all(): 7 | for item in pkgutil.iter_modules(__path__): 8 | if item.ispkg: 9 | yield item.name 10 | 11 | 12 | def build(adapter: str) -> Pipeline: 13 | try: 14 | module = importlib.import_module(f".{adapter}", __package__) 15 | except ImportError: 16 | raise Exception(f"No such adapter {adapter}") 17 | 18 | from . import generic 19 | 20 | initializer = (getattr(module, "getInitializer", None) or generic.getInitializer)() 21 | loader = (getattr(module, "getLoader", None) or generic.getLoader)() 22 | checker = (getattr(module, "getChecker", None) or generic.getChecker)() 23 | packer = (getattr(module, "getPacker", None) or generic.getPacker)() 24 | 25 | return Pipeline(initializer, loader, checker, packer) 26 | -------------------------------------------------------------------------------- /src/generator_oj_problem/adapters/fps/__init__.py: -------------------------------------------------------------------------------- 1 | def getPacker(): 2 | from .packer import Fps 3 | return Fps() 4 | -------------------------------------------------------------------------------- /src/generator_oj_problem/adapters/fps/packer.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from glob import glob 3 | import os 4 | from pathlib import Path 5 | from typing import Iterable 6 | from generator_oj_problem.models import Issue, Problem, Severity, TestCase 7 | from generator_oj_problem.pipelines import Reader, Packer 8 | from generator_oj_problem import __version__ 9 | from yaml import safe_load 10 | from xml.etree.ElementTree import Element, ElementTree, SubElement 11 | from xml.dom.minidom import Document 12 | import mistune 13 | 14 | 15 | class Fps(Packer): 16 | def markdown(self, content: str): 17 | md = mistune.create_markdown( 18 | plugins=["url", "task_lists", "def_list", "abbr"]) 19 | return md(content) 20 | 21 | def pack(self, reader: Reader, dist: Path) -> "Iterable[Issue]": 22 | problem = Problem() 23 | print("Load problem...") 24 | yield from reader.load(problem) 25 | 26 | doc = Document() 27 | 28 | root = doc.createElement("fps") 29 | doc.appendChild(root) 30 | root.setAttribute("url", "https://github.com/zhblue/freeproblemset/") 31 | root.setAttribute("version", "1.2") 32 | 33 | gen = doc.createElement("generator") 34 | root.appendChild(gen) 35 | gen.setAttribute("name", "HUSTOJ") 36 | gen.setAttribute("url", "https://github.com/zhblue/hustoj/") 37 | 38 | item = doc.createElement("item") 39 | root.appendChild(item) 40 | 41 | print("Pack description...") 42 | 43 | sub = doc.createElement("title") 44 | item.appendChild(sub) 45 | sub.appendChild(doc.createCDATASection(problem.name)) 46 | 47 | sub = doc.createElement("time_limit") 48 | item.appendChild(sub) 49 | sub.setAttribute("unit", "s") 50 | sub.appendChild(doc.createCDATASection(str(int(problem.time)))) 51 | 52 | sub = doc.createElement("memory_limit") 53 | item.appendChild(sub) 54 | sub.setAttribute("unit", "mb") 55 | sub.appendChild(doc.createCDATASection(str(int(problem.memory)))) 56 | 57 | sub = doc.createElement("description") 58 | item.appendChild(sub) 59 | sub.appendChild(doc.createCDATASection( 60 | self.markdown(problem.description))) 61 | 62 | sub = doc.createElement("input") 63 | item.appendChild(sub) 64 | sub.appendChild(doc.createCDATASection(self.markdown(problem.input))) 65 | 66 | sub = doc.createElement("output") 67 | item.appendChild(sub) 68 | sub.appendChild(doc.createCDATASection(self.markdown(problem.output))) 69 | 70 | print("Pack sample data...") 71 | 72 | for case in reader.samples(): 73 | print(f" Pack sample {case.name}...") 74 | subin = doc.createElement("sample_input") 75 | subin.appendChild(doc.createCDATASection( 76 | "\n".join(case.input.splitlines()) + "\n")) 77 | 78 | subout = doc.createElement("sample_output") 79 | subout.appendChild(doc.createCDATASection( 80 | "\n".join(case.output.splitlines()) + "\n")) 81 | 82 | item.appendChild(subin) 83 | item.appendChild(subout) 84 | 85 | print("Pack test data...") 86 | 87 | for case in reader.tests(): 88 | print(f" Pack test {case.name}...") 89 | subin = doc.createElement("test_input") 90 | subin.appendChild(doc.createCDATASection( 91 | "\n".join(case.input.splitlines()) + "\n")) 92 | 93 | subout = doc.createElement("test_output") 94 | subout.appendChild(doc.createCDATASection( 95 | "\n".join(case.output.splitlines()) + "\n")) 96 | 97 | item.appendChild(subin) 98 | item.appendChild(subout) 99 | 100 | print("Pack extra...") 101 | 102 | hint = f"""{problem.hint} 103 | 104 | *Generated at {datetime.now()} by [generator-oj-problem](https://github.com/StardustDL/generator-oj-problem) v{__version__}.* 105 | """ 106 | 107 | if not hint.isspace(): 108 | sub = doc.createElement("hint") 109 | item.appendChild(sub) 110 | sub.appendChild(doc.createCDATASection( 111 | self.markdown(hint))) 112 | 113 | if not problem.author.isspace(): 114 | sub = doc.createElement("source") 115 | item.appendChild(sub) 116 | sub.appendChild(doc.createCDATASection(problem.author)) 117 | 118 | if not problem.solution.isspace(): 119 | sub = doc.createElement("solution") 120 | item.appendChild(sub) 121 | sub.setAttribute("language", problem.solutionLanguage) 122 | sub.appendChild(doc.createCDATASection(problem.solution)) 123 | 124 | print("Save dist...") 125 | try: 126 | fp = dist / "fps.xml" 127 | with open(fp, "w", encoding="utf-8") as f: 128 | doc.writexml(f, encoding="utf-8") 129 | yield Issue(f"Saved to {fp}.") 130 | except Exception as ex: 131 | yield Issue(f"Failed to save: {ex}", Severity.Error) 132 | -------------------------------------------------------------------------------- /src/generator_oj_problem/adapters/generic/__init__.py: -------------------------------------------------------------------------------- 1 | def getInitializer(): 2 | from .initializer import Generic 3 | return Generic() 4 | 5 | 6 | def getLoader(): 7 | from .loader import Generic 8 | return Generic() 9 | 10 | 11 | def getChecker(): 12 | from .checker import Generic 13 | return Generic() 14 | 15 | 16 | def getPacker(): 17 | pass 18 | -------------------------------------------------------------------------------- /src/generator_oj_problem/adapters/generic/checker.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import os 3 | from pathlib import Path 4 | from typing import Iterable 5 | from generator_oj_problem.models import Issue, Problem, Severity, TestCase 6 | from generator_oj_problem.pipelines import Reader, Checker 7 | from .paths import PathBuilder 8 | from yaml import safe_load 9 | import chardet 10 | 11 | 12 | class Generic(Checker): 13 | def check(self, reader: Reader) -> "Iterable[Issue]": 14 | problem = Problem() 15 | print("Load problem...") 16 | yield from reader.load(problem) 17 | yield from self._metadata(problem) 18 | yield from self._description(problem) 19 | print("Check sample cases...") 20 | for case in reader.samples(): 21 | yield from self._testcase(problem, case, "sample") 22 | print("Check test cases...") 23 | for case in reader.tests(): 24 | yield from self._testcase(problem, case, "test") 25 | 26 | def _metadata(self, problem: Problem) -> "Iterable[Issue]": 27 | print("Check metadata...") 28 | if problem.name.isspace(): 29 | yield Issue("The name of the problem is missing.", Severity.Error) 30 | if problem.author.isspace(): 31 | yield Issue("The author of the problem is missing.", Severity.Warning) 32 | if problem.time <= 0: 33 | yield Issue("The time limit must be positive.", Severity.Error) 34 | if problem.memory <= 0: 35 | yield Issue("The memory limit must be positive.", Severity.Error) 36 | 37 | def _description(self, problem: Problem) -> "Iterable[Issue]": 38 | print("Check description...") 39 | if problem.description.isspace(): 40 | yield Issue("The description is missing.", Severity.Error) 41 | if problem.input.isspace(): 42 | yield Issue("The input description is missing.", Severity.Warning) 43 | if problem.output.isspace(): 44 | yield Issue("The output description is missing.", Severity.Warning) 45 | if problem.hint.isspace(): 46 | yield Issue("The hint is missing.", Severity.Warning) 47 | if problem.solution.isspace(): 48 | yield Issue("The solution is missing.", Severity.Warning) 49 | 50 | def _testcase(self, problem: Problem, case: TestCase, type: str) -> "Iterable[Issue]": 51 | print(f" Check {type} case {case.name}...") 52 | 53 | if len(case.rinput) > 0 and not chardet.detect(case.rinput).get("encoding", None) in {"utf-8", "ascii"}: 54 | yield Issue(f"The input of {type} {case.name} is not in UTF-8.", Severity.Warning) 55 | if len(case.routput) > 0 and not chardet.detect(case.routput).get("encoding", None) in {"utf-8", "ascii"}: 56 | yield Issue(f"The output of {type} {case.name} is not in UTF-8.", Severity.Warning) 57 | 58 | if case.input.isspace(): 59 | yield Issue(f"The input of {type} {case.name} is missing.", Severity.Error) 60 | if case.output.isspace(): 61 | yield Issue(f"The output of {type} {case.name} is missing.", Severity.Error) 62 | 63 | eolName = r"CRLF(\r\n)" if problem.crlf else r"LF(\n)" 64 | 65 | for i, l in enumerate(case.input.splitlines(keepends=True)): 66 | if l.endswith("\r\n") != problem.crlf: 67 | yield Issue(f"The line {i+1} of {type} {case.name} input is not ended with {eolName}.", Severity.Warning) 68 | for i, l in enumerate(case.output.splitlines(keepends=True)): 69 | if l.endswith("\r\n") != problem.crlf: 70 | yield Issue(f"The line {i+1} of {type} {case.name} output is not ended with {eolName}.", Severity.Warning) 71 | -------------------------------------------------------------------------------- /src/generator_oj_problem/adapters/generic/initializer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Iterable 4 | from generator_oj_problem.models import Issue, Problem, Severity 5 | from generator_oj_problem.pipelines import Initializer 6 | from generator_oj_problem.generators.processors import TestGenerator 7 | from .paths import PathBuilder 8 | from yaml import safe_dump 9 | 10 | 11 | class Generic(Initializer): 12 | def initialize(self, root: Path) -> "Iterable[Issue]": 13 | paths = PathBuilder(root) 14 | 15 | if paths.description.exists(): 16 | yield Issue("Description exists.", Severity.Warning) 17 | else: 18 | paths.description.write_text("""Calculate `a+b`.""") 19 | 20 | if paths.input.exists(): 21 | yield Issue("Input description exists.", Severity.Warning) 22 | else: 23 | paths.input.write_text("""Two integer `a`, `b` (0<=a,b<=10).""") 24 | 25 | if paths.output.exists(): 26 | yield Issue("Output description exists.", Severity.Warning) 27 | else: 28 | paths.output.write_text("""Output `a+b`.""") 29 | 30 | std = """#include 31 | using namespace std; 32 | int main() 33 | { 34 | int a, b; 35 | cin >> a >> b; 36 | cout << a + b << endl; 37 | return 0; 38 | } 39 | """ 40 | 41 | if paths.solution.exists(): 42 | yield Issue("Solution program exists.", Severity.Warning) 43 | else: 44 | paths.solution.write_text(std) 45 | 46 | if paths.hint.exists(): 47 | yield Issue("Hint exists.", Severity.Warning) 48 | else: 49 | paths.hint.write_text(f"""Standard program: 50 | 51 | ```cpp 52 | {std} 53 | ```""") 54 | 55 | if paths.metadata.exists(): 56 | yield Issue("Metadata exists.", Severity.Warning) 57 | else: 58 | paths.metadata.write_text("""# Problem name 59 | name: A + B Problem 60 | # Author or source 61 | author: '' 62 | # Memory limit in MB 63 | memory: 128.0 64 | # Time limit in seconds 65 | time: 1.0 66 | # Use CRLF (\\r\\n) as the end-of-line sequence, set to false to use LF (\\n) 67 | crlf: false 68 | # Programming language used by 'solution.txt' 69 | solutionLanguage: C++ 70 | """) 71 | 72 | if not paths.samples.exists() or paths.samples.is_file(): 73 | os.makedirs(paths.samples) 74 | 75 | if not paths.tests.exists() or paths.tests.is_file(): 76 | os.makedirs(paths.tests) 77 | 78 | def genIO(testdir: Path, name: str, input: str, output: str, type: str): 79 | infile = testdir / f"{name}.in" 80 | outfile = testdir / f"{name}.out" 81 | 82 | if infile.exists() or outfile.exists(): 83 | yield Issue(f"{type.title()} case {name} exists in {testdir}", Severity.Warning) 84 | else: 85 | infile.write_text(input) 86 | outfile.write_text(output) 87 | 88 | yield from genIO(paths.samples, "0", "1 2\n", "3\n", "sample") 89 | yield from genIO(paths.tests, "0", "2 3\n", "5\n", "test") 90 | yield from genIO(paths.tests, "1", "20 22\n", "42\n", "test") 91 | 92 | yield from TestGenerator(root, Problem()).initialize() 93 | -------------------------------------------------------------------------------- /src/generator_oj_problem/adapters/generic/loader.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import os 3 | from pathlib import Path 4 | from typing import Iterable 5 | from generator_oj_problem.models import Issue, Problem, Severity, TestCase 6 | from generator_oj_problem.pipelines import Loader, Reader 7 | from .paths import PathBuilder 8 | from yaml import safe_load 9 | 10 | 11 | class GenericReader(Reader): 12 | def __init__(self, root: Path) -> None: 13 | super().__init__() 14 | self.root = root 15 | self.paths = PathBuilder(root) 16 | 17 | def load(self, problem: Problem) -> "Iterable[Issue]": 18 | paths = self.paths 19 | 20 | if not paths.description.exists(): 21 | yield Issue("Description does NOT exist.", Severity.Warning) 22 | else: 23 | problem.description = paths.description.read_text() 24 | 25 | if not paths.input.exists(): 26 | yield Issue("Input description does NOT exist.", Severity.Warning) 27 | else: 28 | problem.input = paths.input.read_text() 29 | 30 | if not paths.output.exists(): 31 | yield Issue("Output description does NOT exist.", Severity.Warning) 32 | else: 33 | problem.output = paths.output.read_text() 34 | 35 | if not paths.hint.exists(): 36 | yield Issue("Hint does NOT exist.", Severity.Warning) 37 | else: 38 | problem.hint = paths.hint.read_text() 39 | 40 | if not paths.solution.exists(): 41 | yield Issue("Solution does NOT exist.", Severity.Warning) 42 | else: 43 | problem.solution = paths.solution.read_text() 44 | 45 | if not paths.metadata.exists(): 46 | yield Issue("Metadata does NOT exist.", Severity.Warning) 47 | else: 48 | try: 49 | metadata: dict = safe_load(paths.metadata.read_text()) 50 | problem.name = metadata.get("name", "") 51 | problem.author = metadata.get("author", "") 52 | problem.time = float(metadata.get("time", 1.0)) 53 | problem.memory = float(metadata.get("memory", 128.0)) 54 | problem.solutionLanguage = metadata.get( 55 | "solutionLanguage", "C++") 56 | problem.crlf = metadata.get("crlf", False) 57 | except: 58 | yield Issue("Metadata is in wrong format.", Severity.Error) 59 | 60 | return problem 61 | 62 | def _getCases(self, root: Path): 63 | if not root.exists() or root.is_file(): 64 | return 65 | for infile in root.glob("*.in"): 66 | case = TestCase(infile.stem, infile.read_bytes()) 67 | outfile = infile.with_suffix(".out") 68 | if outfile.exists(): 69 | case.routput = outfile.read_bytes() 70 | yield case 71 | 72 | def samples(self) -> Iterable[TestCase]: 73 | return self._getCases(self.paths.samples) 74 | 75 | def tests(self) -> Iterable[TestCase]: 76 | return self._getCases(self.paths.tests) 77 | 78 | 79 | class Generic(Loader): 80 | def build(self, root: Path) -> Reader: 81 | return GenericReader(root) 82 | 83 | def initialize(self, root: Path) -> "Iterable[Issue]": 84 | paths = PathBuilder(root) 85 | 86 | if paths.description.exists(): 87 | yield Issue("Description exists.", Severity.Warning) 88 | else: 89 | paths.description.write_text("""Calculate `a+b`.""") 90 | 91 | if paths.input.exists(): 92 | yield Issue("Input description exists.", Severity.Warning) 93 | else: 94 | paths.input.write_text("""Two integer `a`, `b` (0<=a,b<=10).""") 95 | 96 | if paths.output.exists(): 97 | yield Issue("Output description exists.", Severity.Warning) 98 | else: 99 | paths.output.write_text("""Output `a+b`.""") 100 | 101 | if paths.hint.exists(): 102 | yield Issue("Hint exists.", Severity.Warning) 103 | else: 104 | paths.hint.write_text("""Standard program: 105 | 106 | ```cpp 107 | #include 108 | using namespace std; 109 | int main() 110 | { 111 | int a, b; 112 | cin >> a >> b; 113 | cout << a + b << endl; 114 | return 0; 115 | } 116 | ```""") 117 | 118 | if paths.metadata.exists(): 119 | yield Issue("Metadata exists.", Severity.Warning) 120 | else: 121 | paths.metadata.write_text(safe_dump({ 122 | "name": "A + B Problem", 123 | "author": "", 124 | "timelimit": 1.0, 125 | "memorylimit": 128.0 126 | })) 127 | 128 | if not paths.samples.exists() or paths.samples.is_file(): 129 | os.makedirs(paths.samples) 130 | 131 | if not paths.tests.exists() or paths.tests.is_file(): 132 | os.makedirs(paths.tests) 133 | 134 | def genIO(testdir: Path, name: str, input: str, output: str): 135 | infile = testdir / f"{name}.in" 136 | outfile = testdir / f"{name}.out" 137 | 138 | if infile.exists() or outfile.exists(): 139 | yield Issue(f"Test case {name} exists in {testdir}") 140 | else: 141 | infile.write_text(input) 142 | outfile.write_text(output) 143 | 144 | yield from genIO(paths.samples, "0", "1 2\n", "3\n") 145 | yield from genIO(paths.tests, "0", "2 3\n", "5\n") 146 | yield from genIO(paths.tests, "1", "20 22\n", "42\n") 147 | -------------------------------------------------------------------------------- /src/generator_oj_problem/adapters/generic/paths.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | class PathBuilder: 5 | def __init__(self, root: Path) -> None: 6 | self.root = root 7 | self.description = root / "description.md" 8 | self.input = root / "input.md" 9 | self.output = root / "output.md" 10 | self.hint = root / "hint.md" 11 | self.metadata = root / "problem.yml" 12 | self.samples = root / "samples" 13 | self.tests = root / "tests" 14 | self.solution = root / "solution.txt" 15 | -------------------------------------------------------------------------------- /src/generator_oj_problem/adapters/hustoj/__init__.py: -------------------------------------------------------------------------------- 1 | def getPacker(): 2 | from ..fps.packer import Fps 3 | return Fps() 4 | -------------------------------------------------------------------------------- /src/generator_oj_problem/generators/__init__.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import io 3 | import os 4 | from pathlib import Path 5 | import sys 6 | 7 | 8 | ENV_CASE_ID = "GOP_GENERATOR_CASE" 9 | ENV_TARGET = "GOP_GENERATOR_TARGET" 10 | ENV_REWRITE = "GOP_GENERATOR_REWRITE" 11 | ENV_CRLF = "GOP_GENERATOR_CRLF" 12 | SUBMITED = "cfdf14756a566e9e3de87c1980d2fc715032276e" 13 | 14 | 15 | @dataclass 16 | class Case: 17 | id: str = 0 18 | target: Path = Path(".") 19 | input: "str | None" = None 20 | output: "str | None" = None 21 | rewrite: bool = False 22 | crlf: bool = False 23 | 24 | @property 25 | def newline(self): 26 | return "\r\n" if self.crlf else "\n" 27 | 28 | def In(self, *args, **kwargs): 29 | if self.input is None: 30 | self.input = "" 31 | with io.StringIO(self.input, newline=self.newline) as to: 32 | to.seek(len(self.input)) 33 | print(*args, file=to, **kwargs) 34 | self.input = to.getvalue() 35 | 36 | def Out(self, *args, **kwargs): 37 | if self.output is None: 38 | self.output = "" 39 | with io.StringIO(self.output, newline=self.newline) as to: 40 | to.seek(len(self.output)) 41 | print(*args, file=to, **kwargs) 42 | self.output = to.getvalue() 43 | 44 | @property 45 | def infile(self): 46 | return (self.target / f"{self.id}.in") 47 | 48 | @property 49 | def outfile(self): 50 | return (self.target / f"{self.id}.out") 51 | 52 | def submit(self, silence: bool = False): 53 | if self.input is not None: 54 | if self.infile.exists(): 55 | if self.rewrite: 56 | if not silence: 57 | print( 58 | f"Rewrite existed input file {self.infile}.", file=sys.stderr) 59 | else: 60 | if not silence: 61 | print( 62 | f"Input file {self.infile} exists.", file=sys.stderr) 63 | exit(1) 64 | self.infile.write_bytes(self.input.encode("utf-8")) 65 | if self.output is not None: 66 | if self.outfile.exists(): 67 | if self.rewrite: 68 | if not silence: 69 | print( 70 | f"Rewrite existed output file {self.outfile}.", file=sys.stderr) 71 | else: 72 | if not silence: 73 | print( 74 | f"Output file {self.outfile} exists.", file=sys.stderr) 75 | exit(1) 76 | self.outfile.write_bytes(self.output.encode("utf-8")) 77 | if not silence: 78 | print(SUBMITED, end="") 79 | 80 | 81 | data: Case = Case() 82 | 83 | try: 84 | data.id = str(os.getenv(ENV_CASE_ID) or 0) 85 | data.target = Path(os.getenv(ENV_TARGET) or ".") 86 | data.rewrite = os.getenv(ENV_REWRITE) == "1" 87 | data.crlf = os.getenv(ENV_CRLF) == "1" 88 | except: 89 | pass 90 | -------------------------------------------------------------------------------- /src/generator_oj_problem/generators/processors.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from pathlib import Path 4 | import sys 5 | from typing import Iterable 6 | 7 | from generator_oj_problem.models import Issue, Problem, Severity 8 | from generator_oj_problem.pipelines import Reader 9 | from . import ENV_CASE_ID, ENV_CRLF, ENV_REWRITE, ENV_TARGET, SUBMITED, Case 10 | 11 | 12 | class TestGenerator: 13 | def __init__(self, root: Path, reader: Reader) -> None: 14 | self.reader = reader 15 | self.root = root 16 | self.file = root / "generator.py" 17 | 18 | def initialize(self) -> "Iterable[Issue]": 19 | if self.file.exists(): 20 | yield Issue("Generator exists.", Severity.Warning) 21 | else: 22 | self.file.write_text( 23 | (Path(__file__).parent / "template.py").read_text()) 24 | 25 | def generate(self, start: int, count: int, sample: bool = False, rewrite: bool = False) -> "Iterable[Issue]": 26 | if not self.file.exists() or self.file.is_dir(): 27 | yield Issue("Generator is not found.", Severity.Error) 28 | return 29 | 30 | problem = Problem() 31 | yield from self.reader.load(problem) 32 | 33 | prefix = "sample" if sample else "test" 34 | target = self.root / f"{prefix}s" 35 | if not target.exists() or target.is_file(): 36 | os.makedirs(target) 37 | for i in range(start, start + count): 38 | case = Case(i, target) 39 | print(f"Generate {prefix} case {i}...") 40 | try: 41 | result = subprocess.run(["-u", str(self.file)], 42 | executable=sys.executable, 43 | cwd=self.root, 44 | text=True, 45 | capture_output=True, 46 | env={**os.environ, 47 | ENV_CASE_ID: str(case.id), 48 | ENV_TARGET: str(case.target.resolve()), 49 | ENV_REWRITE: "1" if rewrite else "0", 50 | ENV_CRLF: "1" if problem.crlf else "0", 51 | "PYTHONUTF8": "1"}) 52 | stdout = result.stdout 53 | if stdout is None or not stdout.endswith(SUBMITED): 54 | yield Issue(f"Generated data is not submitted for {prefix} case {case.id}, please call 'data.submit()' at the end of generator.", Severity.Warning) 55 | else: 56 | stdout = stdout.replace(SUBMITED, "") 57 | if stdout: 58 | yield Issue(f"Generator standard output for {prefix} case {case.id}:\n{stdout.strip()}", Severity.Info) 59 | if result.stderr: 60 | yield Issue(f"Generator standard error for {prefix} case {case.id}:\n{result.stderr.strip()}", Severity.Warning) 61 | if result.returncode != 0: 62 | raise Exception( 63 | f"Generator exited with non-zero: {result.returncode}.") 64 | yield Issue(f"Generated {prefix} case {case.id}.") 65 | except Exception as ex: 66 | yield Issue(f"Failed to generate {prefix} case {case.id}: {ex}", Severity.Error) 67 | -------------------------------------------------------------------------------- /src/generator_oj_problem/generators/template.py: -------------------------------------------------------------------------------- 1 | # Import the data container from generator-oj-problem 2 | from generator_oj_problem.generators import data 3 | 4 | # Write your generator code 5 | from random import randint 6 | x, y = randint(1, 1000), randint(1, 1000) 7 | 8 | # Write to input data, just like `print(x, y, file=input)` 9 | data.In(x, y) 10 | 11 | # Write to output data, just like `print(x, y, file=output)` 12 | data.Out(x + y) 13 | 14 | # Always keep this invocation at the end of the generator 15 | # to submit your data. 16 | data.submit() 17 | -------------------------------------------------------------------------------- /src/generator_oj_problem/generators/trimmers.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Iterable 3 | 4 | from ..models import Issue, Problem, Severity, TestCase 5 | from ..pipelines import Reader 6 | from . import Case 7 | 8 | 9 | class TestTrimmer: 10 | def __init__(self, root: Path, reader: Reader) -> None: 11 | self.root = root 12 | self.reader = reader 13 | 14 | def trim(self) -> "Iterable[Issue]": 15 | problem = Problem() 16 | yield from self.reader.load(problem) 17 | 18 | def item(case: TestCase, type: str): 19 | print(f" Trim {type} case {case.name}...") 20 | target = Case(case.name, self.root / 21 | f"{type}s", rewrite=True, crlf=problem.crlf) 22 | for l in case.input.rstrip().rstrip("\r\n").splitlines(): 23 | target.In(l.rstrip("\r\n")) 24 | for l in case.output.rstrip().rstrip("\r\n").splitlines(): 25 | target.Out(l.rstrip("\r\n")) 26 | target.submit(silence=True) 27 | yield Issue(f"Trimmed {type} case {case.name}.", Severity.Info) 28 | 29 | for case in self.reader.samples(): 30 | yield from item(case, "sample") 31 | 32 | for case in self.reader.tests(): 33 | yield from item(case, "test") 34 | -------------------------------------------------------------------------------- /src/generator_oj_problem/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from enum import IntEnum 3 | 4 | 5 | class Severity(IntEnum): 6 | Info = 0 7 | Warning = 10 8 | Error = 20 9 | 10 | 11 | @dataclass 12 | class Issue: 13 | message: str 14 | level: Severity = Severity.Info 15 | 16 | 17 | @dataclass 18 | class TestCase: 19 | name: str = "" 20 | rinput: bytes = b"" 21 | routput: bytes = b"" 22 | 23 | @property 24 | def input(self): 25 | return self.rinput.decode("utf-8") 26 | 27 | @property 28 | def output(self): 29 | return self.routput.decode("utf-8") 30 | 31 | 32 | @dataclass 33 | class Problem: 34 | name: str = "" 35 | author: str = "" 36 | time: float = 1.0 37 | memory: float = 128.0 38 | solution: str = "" 39 | solutionLanguage: str = "C++" 40 | crlf: bool = False 41 | 42 | description: str = "" 43 | input: str = "" 44 | output: str = "" 45 | hint: str = "" 46 | -------------------------------------------------------------------------------- /src/generator_oj_problem/pipelines.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Iterable 4 | 5 | from .models import Issue, Problem, Severity, TestCase 6 | from . import getWorkingDirectory 7 | 8 | 9 | class Initializer: 10 | def initialize(self, root: Path) -> "Iterable[Issue]": 11 | pass 12 | 13 | 14 | class Reader: 15 | def load(self, problem: Problem) -> "Iterable[Issue]": 16 | pass 17 | 18 | def samples(self) -> Iterable[TestCase]: 19 | pass 20 | 21 | def tests(self) -> Iterable[TestCase]: 22 | pass 23 | 24 | 25 | class Loader: 26 | def build(self, root: Path) -> Reader: 27 | pass 28 | 29 | 30 | class Checker: 31 | def check(self, reader: Reader) -> "Iterable[Issue]": 32 | pass 33 | 34 | 35 | class Packer: 36 | def pack(self, reader: Reader, dist: Path) -> "Iterable[Issue]": 37 | pass 38 | 39 | 40 | class Pipeline: 41 | def __init__(self, initializer: "Initializer | None", loader: "Loader | None", checker: "Checker | None", packer: "Packer | None") -> None: 42 | self.initializer = initializer 43 | self.loader = loader 44 | self.checker = checker 45 | self.packer = packer 46 | self.root = getWorkingDirectory() 47 | 48 | def initialize(self): 49 | if self.initializer is None: 50 | yield Issue("The initializer is disabled.", Severity.Error) 51 | else: 52 | yield from self.initializer.initialize(self.root) 53 | 54 | def check(self): 55 | if self.loader is None: 56 | yield Issue("The loader is disabled.", Severity.Error) 57 | elif self.checker is None: 58 | yield Issue("The checker is disabled.", Severity.Error) 59 | else: 60 | yield from self.checker.check(self.loader.build(self.root)) 61 | 62 | def pack(self): 63 | if self.loader is None: 64 | yield Issue("The loader is disabled.", Severity.Error) 65 | elif self.packer is None: 66 | yield Issue("The packer is disabled.", Severity.Error) 67 | else: 68 | dist = self.root / "dist" 69 | if not dist.exists() or dist.is_file(): 70 | os.makedirs(dist) 71 | yield from self.packer.pack(self.loader.build(self.root), dist) 72 | 73 | def generate(self, start: int, count: int, sample: bool = False, rewrite: bool = False): 74 | if self.loader is None: 75 | yield Issue("The loader is disabled.", Severity.Error) 76 | else: 77 | from generator_oj_problem.generators.processors import TestGenerator 78 | yield from TestGenerator(self.root, self.loader.build(self.root)).generate(start, count, sample, rewrite) 79 | 80 | def trim(self): 81 | if self.loader is None: 82 | yield Issue("The loader is disabled.", Severity.Error) 83 | else: 84 | from generator_oj_problem.generators.trimmers import TestTrimmer 85 | yield from TestTrimmer(self.root, self.loader.build(self.root)).trim() 86 | -------------------------------------------------------------------------------- /src/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=51", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.3 2 | PyYAML==6.0 3 | mistune==2.0.4 4 | chardet==5.0.0 -------------------------------------------------------------------------------- /src/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = generator-oj-problem 3 | version = attr: generator_oj_problem.__version__ 4 | author = StardustDL 5 | author_email = stardustdl@163.com 6 | description = A command-line tool to generate Online-Judge problem. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/StardustDL/generator-oj-problem 10 | project_urls = 11 | Source Code = https://github.com/StardustDL/generator-oj-problem 12 | Changes = https://github.com/StardustDL/generator-oj-problem/releases 13 | Bug Tracker = https://github.com/StardustDL/generator-oj-problem/issues 14 | classifiers = 15 | License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) 16 | Operating System :: OS Independent 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.7 19 | Programming Language :: Python :: 3.8 20 | Programming Language :: Python :: 3.9 21 | Programming Language :: Python :: 3.10 22 | Programming Language :: Python :: 3.11 23 | Programming Language :: Python :: Implementation :: CPython 24 | Topic :: Utilities 25 | 26 | [options] 27 | packages = find: 28 | python_requires = >=3.7 29 | install_requires = 30 | click>=8.0 31 | PyYAML>=6.0 32 | mistune>=2.0.0 33 | chardet>=5.0.0 34 | 35 | [options.entry_points] 36 | console_scripts = 37 | gop = generator_oj_problem.__main__:main --------------------------------------------------------------------------------