├── requirements.txt ├── config └── config.yml ├── src ├── init_exec.py ├── scheduled_exec.py └── autoclone.py ├── LICENSE ├── .github └── workflows │ ├── manual_update.yml │ └── scheduled_update.yml ├── doc └── README_en.md ├── README.md └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | pyyaml 3 | requests 4 | -------------------------------------------------------------------------------- /config/config.yml: -------------------------------------------------------------------------------- 1 | user_id : # you must configure your atcoder id e.g.) chokudai -------------------------------------------------------------------------------- /src/init_exec.py: -------------------------------------------------------------------------------- 1 | from autoclone import AutoClone 2 | 3 | if __name__ == "__main__": 4 | time_range = (3600 * 24) * 7 * 3 # three weeks 5 | AutoClone(time_range)() -------------------------------------------------------------------------------- /src/scheduled_exec.py: -------------------------------------------------------------------------------- 1 | from autoclone import AutoClone 2 | 3 | if __name__ == "__main__": 4 | time_range = (3600 * 24) * 2 # two days 5 | AutoClone(time_range)() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hiroto Kurita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/manual_update.yml: -------------------------------------------------------------------------------- 1 | name: Manual Update 2 | on: [ workflow_dispatch ] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses : actions/checkout@v2 9 | - name : Setup Python 10 | uses : actions/setup-python@v2 11 | with : 12 | python-version : "3.8" 13 | 14 | - name : Install Dependencies 15 | run : | 16 | python -m pip install --upgrade pip 17 | pip install -r requirements.txt 18 | 19 | - name : Run AtCoder AutoClone 20 | run : | 21 | python src/init_exec.py 22 | ls 23 | 24 | - name : Commit and Push 25 | id : git_update 26 | continue-on-error: true 27 | run : | 28 | git config user.name "AutoCloneBot" 29 | git config user.email "acautoclone@bot.com" 30 | git add . 31 | git commit -m "Added my codes from AtCoder" 32 | git pull 33 | git push origin main 34 | 35 | - name : Error Handling 36 | if : ${{steps.git_update.outcome == 'failure'}} 37 | run : | 38 | echo "No new submissions found. Your repo were not updated" 39 | -------------------------------------------------------------------------------- /.github/workflows/scheduled_update.yml: -------------------------------------------------------------------------------- 1 | name: Scheduled Update 2 | on: 3 | schedule: 4 | - cron: '45 14 * * *' 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses : actions/checkout@v2 11 | - name : Setup Python 12 | uses : actions/setup-python@v2 13 | with : 14 | python-version : "3.8" 15 | 16 | - name : Install Dependencies 17 | run : | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt 20 | 21 | - name : Run AtCoder AutoClone 22 | run : | 23 | python src/init_exec.py 24 | ls 25 | 26 | - name : Commit and Push 27 | id : git_update 28 | continue-on-error: true 29 | run : | 30 | git config user.name "AutoCloneBot" 31 | git config user.email "acautoclone@bot.com" 32 | git add . 33 | git commit -m "Added my codes from AtCoder" 34 | git pull 35 | git push origin main 36 | 37 | - name : Error Handling 38 | if : ${{steps.git_update.outcome == 'failure'}} 39 | run : | 40 | echo "No new submissions found. Your repo were not updated" 41 | -------------------------------------------------------------------------------- /doc/README_en.md: -------------------------------------------------------------------------------- 1 | # AtCoder-AutoClone 2 | Automatically clone your AtCoder submissions to your GitHub🚀 3 | 4 | ## Usage 5 | 1. Folk this repository to your GitHub. You can set the repo name and description whatever you like: 6 | ![image](https://user-images.githubusercontent.com/73727292/164982931-0dd13ef4-323e-4e38-9fd2-318a212d9be9.png) 7 | 8 | 2. In your folked repository, go and edit `config/config.yml` and configure your AtCoder ID. 9 | 10 | 3. Go to "actions" tab in your folked repository, and enable workflow![image](https://user-images.githubusercontent.com/73727292/164983120-b7e0f190-4122-4aa4-a186-04687ec0dfb7.png) 11 | 12 | 4. For the initialization, run `Manual Update` workflow on your `main` branch, then your latest 3-weeks of submissions will automatically push to your repo🚀 ![image](https://user-images.githubusercontent.com/73727292/164983404-841a43b9-ef4e-4570-aa23-599d83b170c8.png) 13 | 14 | 5. Your codes will be cloned every night (around 11:45pm JST) to your repository, if you have new submissions😄 15 | 16 | ## Licence 17 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details 18 | ## Acknowledgments 19 | This project is built on [ 20 | kenkoooo](https://github.com/kenkoooo)'s [AtCoder Problems](https://kenkoooo.com/atcoder/) API. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AtCoder-AutoClone 2 | Automatically clone your AtCoder submissions to your GitHub🚀 3 | EN : [README_EN](./doc/README_en.md) 4 | 5 | ## Usage 6 | 1. このレポジトリを自分のGitHubにFolkしてください. レポジトリ名は自分の好きなもの(例:`MyAtCoder-Submissions`)を設定してOKです: 7 | ![image](https://user-images.githubusercontent.com/73727292/164982931-0dd13ef4-323e-4e38-9fd2-318a212d9be9.png) 8 | 9 | 2. Folkされたレポジトリで,`config/config.yml`を編集してAtCoderのユーザー名を登録してください. 10 | 11 | 3. GitHub上の"actions"タブを開き, workflowを有効化してくさい.![image](https://user-images.githubusercontent.com/73727292/164983120-b7e0f190-4122-4aa4-a186-04687ec0dfb7.png) 12 | 13 | 14 | 4. 初期化のために,`Manual Update`を`main`ブランチに対して作動させてください. 直近3週間の提出コード(ACコード)が自動で取得されます🚀 ![image](https://user-images.githubusercontent.com/73727292/164983404-841a43b9-ef4e-4570-aa23-599d83b170c8.png) 15 | 16 | 5. 毎晩11時45分頃に新しい提出が自動であなたのレポジトリに更新されます!🚀😄 17 | 18 | 6. (Optional) このREADMEを削除して,オリジナルのREADMEに差し替えてください.    19 | 20 | 使用例 : https://github.com/kuriyan1204/AtCoder-Submissions 21 | 22 | ## Contribution 23 | もし不具合や追加して欲しい機能があれば気軽にissueを立ててください🤗 24 | 25 | ## Licence 26 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details 27 | ## Acknowledgments 28 | This project is built on [ 29 | kenkoooo](https://github.com/kenkoooo)'s [AtCoder Problems](https://kenkoooo.com/atcoder/) API. 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /src/autoclone.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | from datetime import datetime 4 | 5 | from bs4 import BeautifulSoup 6 | import requests 7 | 8 | CONFIG_PATH = "config/config.yml" 9 | PROBLEMS_API_ENDPOINT = "https://kenkoooo.com/atcoder/atcoder-api/v3/user/submissions" 10 | EXTENSIONS = { 11 | "C++": "cpp", 12 | "Bash": "sh", 13 | "C#": "cs", 14 | "JavaScript": "js", 15 | "OpenJDK": "java", 16 | "Haskell": "hs", 17 | "OCaml": "ml", 18 | "Perl": "pl", 19 | "PHP": "php", 20 | "Python": "py", 21 | "PyPy": "py", 22 | "Pascal": "pas", 23 | "Perl": "pl", 24 | "Ruby": "rb", 25 | "Scala": "scala", 26 | "Text": "txt", 27 | "Visual Basic": "vb", 28 | "Objective-C": "m", 29 | "Swift": "swift", 30 | "Rust": "rs", 31 | "Sed": "sed", 32 | "Awk": "awk", 33 | "Brainfuck": "bf", 34 | "Standard ML": "ml", 35 | "Crystal": "cr", 36 | "Julia": "jl", 37 | "Octave": "m", 38 | "Nim": "nim", 39 | "TypeScript": "ts", 40 | "Perl6": "p6", 41 | "Kotlin": "kt", 42 | "COBOL": "cob", 43 | "C": ".c", 44 | } 45 | 46 | 47 | class AutoClone(object): 48 | """ 49 | AtCoder AutoClone Class 50 | 51 | Attributes 52 | ---------- 53 | user_id : str 54 | AtCoder user_id from yml file 55 | time_range : int 56 | range of time for submission query (seconds). 57 | e.g. : 3600 -> get submission of (current-3600 sec ~ current) 58 | cur_unix_time : int 59 | current unix time 60 | ac_only : bool 61 | flag whether to save non-ac code. this feature is currently disabled 62 | submissions : list of dict 63 | request result from AtCoder Problems Submission API 64 | """ 65 | 66 | def __init__(self, time_range): 67 | self.user_id = self.load_yml()["user_id"] 68 | self.time_range = time_range 69 | self.cur_unix_time = int(datetime.timestamp(datetime.now())) 70 | self.ac_only = True # future todo 71 | 72 | if self.user_id is None: 73 | raise Exception("user_id not found. you must configure config/config.yml") 74 | 75 | def get_submissions(self) -> None: 76 | """ 77 | Get submission information via AtCoder Problems API 78 | 79 | Returns 80 | ------- 81 | submissions : dict 82 | """ 83 | unix_time = self.cur_unix_time - self.time_range 84 | params = {"user": self.user_id, "from_second": unix_time} 85 | result = requests.get(url=PROBLEMS_API_ENDPOINT, params=params) 86 | 87 | if not result.status_code == 200: 88 | raise Exception(f"{result.status_code} : Something went wrong") 89 | self.submissions = result.json() 90 | 91 | def get_and_write_submitted_codes(self) -> None: 92 | """ 93 | Scrape AtCoder's submission pages to get codes 94 | & save write codes as new file 95 | """ 96 | for record in self.submissions: 97 | contest_id = record["contest_id"] 98 | language = record["language"] 99 | problem_id = record["problem_id"] 100 | submission_id = record["id"] 101 | result = record["result"] 102 | 103 | if self.ac_only and result == "AC": 104 | code = self.get_code(contest_id, submission_id) 105 | self.write_code(code, contest_id, problem_id, language) 106 | else: 107 | # Accept non-AC reuslt 108 | # Currently Unavailable 109 | pass 110 | 111 | def __call__(self): 112 | """ 113 | Excecute AutoClone 114 | """ 115 | self.get_submissions() 116 | self.get_and_write_submitted_codes() 117 | 118 | @staticmethod 119 | def get_code(contest_id: str, submission_id: int) -> str: 120 | """ 121 | Get code from AtCoder page 122 | 123 | Parameters 124 | ---------- 125 | contest_id : str 126 | target contest_id 127 | submission_id : int 128 | target submissio_id 129 | 130 | Returns 131 | ------- 132 | str 133 | str of raw code without extension 134 | """ 135 | submission_url = ( 136 | f"https://atcoder.jp/contests/{contest_id}/submissions/{submission_id}" 137 | ) 138 | return BeautifulSoup( 139 | requests.get(submission_url).content, "html.parser" 140 | ).pre.string 141 | 142 | @staticmethod 143 | def write_code(code, contest_id, problem_id, language) -> None: 144 | """ 145 | Write code as new file 146 | 147 | Parameter 148 | --------- 149 | code : str 150 | str of raw code without extension 151 | contest_id : str 152 | target contest_id. used as folder name 153 | problem_id : str 154 | target problem_id. used as file name 155 | language : str 156 | target programming language (not extension) 157 | """ 158 | extension = AutoClone.get_extension(language) 159 | path = f"{contest_id}/{problem_id}.{extension}" 160 | os.makedirs(os.path.dirname(path), exist_ok=True) 161 | with open(path, "w") as f: 162 | f.write(code) 163 | 164 | @staticmethod 165 | def load_yml() -> dict: 166 | """ 167 | Load yml config file 168 | 169 | Returns 170 | ------- 171 | config : dict 172 | dict of config file. 173 | """ 174 | with open(CONFIG_PATH, "r") as f: 175 | config = yaml.safe_load(f) 176 | return config 177 | 178 | @staticmethod 179 | def get_extension(language: str) -> str: 180 | """ 181 | Get extension of specified programming language 182 | 183 | Parameters 184 | ---------- 185 | language : str 186 | target programming language (not extension) 187 | 188 | Returns 189 | ------- 190 | extension : str 191 | file extension of the target language 192 | """ 193 | extension = None 194 | for lang in EXTENSIONS.keys(): 195 | if lang in language: 196 | extension = EXTENSIONS[lang] 197 | break 198 | if extension is None: 199 | raise Exception( 200 | f"Extension for {language} did not found. Please contact @kuriyan1204 via GitHub" 201 | ) 202 | return extension 203 | 204 | 205 | if __name__ == "__main__": 206 | pass 207 | --------------------------------------------------------------------------------