Skip to content

Commit c385e70

Browse files
Miłosz SkazaColdHeat
Miłosz Skaza
andauthored
Refactor CTFCLI to more OOP approach (#130)
* Refactor ctfcli structure * Switch to poetry --------- Co-authored-by: Kevin Chung <[email protected]>
1 parent 7b4a09a commit c385e70

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+8782
-1641
lines changed

.editorconfig

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
root = true
2+
3+
[*]
4+
end_of_line = lf
5+
insert_final_newline = true
6+
7+
[*.py]
8+
charset = utf-8
9+
indent_style = space
10+
indent_size = 4
11+
12+
[Makefile]
13+
indent_style = tab

.github/workflows/lint.yml

+6-7
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,24 @@ on: [push, pull_request]
55

66
jobs:
77
build:
8-
98
runs-on: ubuntu-latest
109

1110
strategy:
1211
matrix:
13-
python-version: ['3.8']
12+
python-version: ['3.11']
1413

1514
name: Linting
1615
steps:
17-
- uses: actions/checkout@v2
16+
- uses: actions/checkout@v3
1817
- name: Setup python
19-
uses: actions/setup-python@v2
18+
uses: actions/setup-python@v4
2019
with:
2120
python-version: ${{ matrix.python-version }}
2221
architecture: x64
2322
- name: Install dependencies
2423
run: |
25-
python -m pip install --upgrade pip
26-
python -m pip install -r development.txt
24+
pip install poetry
25+
poetry install
2726
2827
- name: Lint
29-
run: make lint
28+
run: poetry run make lint

.github/workflows/release.yml

+7-7
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,28 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
python-version: [3.8]
17+
python-version: [3.11]
1818

1919
# Steps represent a sequence of tasks that will be executed as part of the job
2020
steps:
2121
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
2222
- uses: actions/checkout@v2
2323

2424
# Set up Python environment
25-
- name: Set up Python 3.8
26-
uses: actions/setup-python@v2
25+
- name: Set up Python
26+
uses: actions/setup-python@v4
2727
with:
28-
python-version: 3.8
28+
python-version: 3.11
2929

3030
# Install dependencies
3131
- name: Install dependencies
3232
run: |
33-
python -m pip install --upgrade pip
34-
pip install -r development.txt
33+
pip install poetry
34+
poetry install
3535
3636
# Build wheels
3737
- name: Build wheels
38-
run: python setup.py sdist bdist_wheel
38+
run: poetry run python setup.py sdist bdist_wheel
3939

4040
- uses: actions/upload-artifact@v2
4141
with:

.github/workflows/test.yml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
name: Testing
3+
4+
on: [push, pull_request]
5+
6+
jobs:
7+
build:
8+
runs-on: ubuntu-latest
9+
10+
strategy:
11+
matrix:
12+
python-version: ['3.8', '3.9', '3.10', '3.11']
13+
14+
name: Testing
15+
steps:
16+
- uses: actions/checkout@v3
17+
- name: Setup python
18+
uses: actions/setup-python@v4
19+
with:
20+
python-version: ${{ matrix.python-version }}
21+
architecture: x64
22+
- name: Install dependencies
23+
run: |
24+
pip install poetry
25+
poetry install
26+
27+
- name: Test
28+
run: poetry run make test

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,4 @@ dmypy.json
134134
.idea/
135135

136136
.ctf/
137+
!tests/fixtures/challenges/.ctf

.gitlab-ci.yml

-11
This file was deleted.

MANIFEST.in

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ include README.md
33
recursive-include ctfcli/templates *
44
recursive-include ctfcli/utils *
55
recursive-include ctfcli/spec *
6+
recursive-exclude tests *

Makefile

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
lint:
2-
ruff check --ignore=E402,E501,E712,I002 --exclude=ctfcli/templates --exclude=build .
3-
black --check --exclude=ctfcli/templates .
2+
black --check .
3+
isort --check .
4+
ruff check .
45

56
format:
6-
black --exclude=ctfcli/templates .
7+
black .
8+
isort .
9+
ruff --fix .
10+
11+
test:
12+
pytest --cov=ctfcli tests
713

814
install:
915
python3 setup.py install
@@ -15,6 +21,8 @@ clean:
1521
rm -rf build/
1622
rm -rf dist/
1723
rm -rf ctfcli.egg-info/
24+
rm -rf .ruff_cache
25+
rm -f .coverage
1826

1927
publish-test:
2028
@echo "Publishing to TestPyPI"
@@ -26,4 +34,4 @@ publish-pypi:
2634
@echo "Publishing to PyPI"
2735
@echo "ARE YOU ABSOLUTELY SURE? [y/N] " && read ans && [ $${ans:-N} == y ]
2836
python3 setup.py sdist bdist_wheel
29-
twine upload --repository pypi dist/*
37+
twine upload --repository pypi dist/*

ctfcli/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = "0.0.13"
1+
__version__ = "0.1.0"
22
__name__ = "ctfcli"

ctfcli/__main__.py

+98-47
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,102 @@
11
import configparser
2+
import logging
3+
import os
24
import subprocess
3-
5+
import sys
46
from pathlib import Path
7+
from typing import Optional, Union
58

69
import click
710
import fire
811

9-
from ctfcli.cli.challenges import Challenge
10-
from ctfcli.cli.config import Config
11-
from ctfcli.cli.plugins import Plugins
12-
from ctfcli.cli.templates import Templates
13-
from ctfcli.cli.pages import Pages
14-
from ctfcli.utils.plugins import load_plugins
12+
from ctfcli.cli.challenges import ChallengeCommand
13+
from ctfcli.cli.config import ConfigCommand
14+
from ctfcli.cli.pages import PagesCommand
15+
from ctfcli.cli.plugins import PluginsCommand
16+
from ctfcli.cli.templates import TemplatesCommand
17+
from ctfcli.core.exceptions import ProjectNotInitialized
18+
from ctfcli.core.plugins import load_plugins
1519
from ctfcli.utils.git import check_if_dir_is_inside_git_repo
1620

21+
# Init logging
22+
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO").upper())
23+
24+
log = logging.getLogger("ctfcli.main")
25+
26+
27+
class CTFCLI:
28+
@staticmethod
29+
def init(
30+
directory: Optional[Union[str, os.PathLike]] = None,
31+
no_git: bool = False,
32+
no_commit: bool = False,
33+
):
34+
log.debug(f"init: (directory={directory}, no_git={no_git}, no_commit={no_commit})")
35+
project_path = Path.cwd()
1736

18-
class CTFCLI(object):
19-
def init(self, directory=None, no_config=False, no_git=False):
20-
# Create our event directory if requested and use it as our base directory
37+
# Create our project directory if requested
2138
if directory:
22-
path = Path(directory)
23-
path.mkdir()
24-
click.secho(f"Created empty directory in {path.absolute()}", fg="green")
25-
else:
26-
path = Path(".")
39+
project_path = Path(directory)
2740

28-
# Get variables from user
29-
ctf_url = click.prompt(
30-
"Please enter CTFd instance URL", default="", show_default=False
31-
)
32-
ctf_token = click.prompt(
33-
"Please enter CTFd Admin Access Token", default="", show_default=False
34-
)
35-
# Confirm information with user
36-
if (
37-
click.confirm(f"Do you want to continue with {ctf_url} and {ctf_token}")
38-
is False
39-
):
40-
click.echo("Aborted!")
41-
return
41+
if not project_path.exists():
42+
project_path.mkdir(parents=True)
43+
click.secho(f"Created empty directory in {project_path.absolute()}", fg="green")
4244

4345
# Avoid colliding with existing .ctf directory
44-
if (path / ".ctf").exists():
46+
if (project_path / ".ctf").exists():
4547
click.secho(".ctf/ folder already exists. Aborting!", fg="red")
4648
return
4749

50+
log.debug(f"project_path: {project_path}")
51+
4852
# Create .ctf directory
49-
(path / ".ctf").mkdir()
53+
(project_path / ".ctf").mkdir()
54+
55+
# Get variables from user
56+
ctf_url = click.prompt("Please enter CTFd instance URL", default="", show_default=False)
57+
58+
ctf_token = click.prompt("Please enter CTFd Admin Access Token", default="", show_default=False)
59+
60+
# Confirm information with user
61+
if not click.confirm(f"Do you want to continue with {ctf_url} and {ctf_token}", default=True):
62+
click.echo("Aborted!")
63+
return
5064

5165
# Create initial .ctf/config file
5266
config = configparser.ConfigParser()
5367
config["config"] = {"url": ctf_url, "access_token": ctf_token}
5468
config["challenges"] = {}
55-
with (path / ".ctf" / "config").open(mode="a+") as f:
56-
config.write(f)
69+
with (project_path / ".ctf" / "config").open(mode="a+") as config_file:
70+
config.write(config_file)
5771

58-
# Create a git repo in the event folder
59-
if check_if_dir_is_inside_git_repo(dir=path.absolute()) is True:
60-
click.secho("Already in git repo. Skipping git init.", fg="yellow")
61-
elif no_git is True:
72+
# if git init is to be skipped we can return
73+
if no_git:
6274
click.secho("Skipping git init.", fg="yellow")
63-
else:
64-
click.secho(f"Creating git repo in {path.absolute()}", fg="green")
65-
subprocess.call(["git", "init", str(path)])
75+
return
76+
77+
# also skip git init if git is already initialized
78+
if check_if_dir_is_inside_git_repo(cwd=project_path):
79+
click.secho("Already in a git repo. Skipping git init.", fg="yellow")
80+
81+
# is git commit is to be skipped we can return
82+
if no_commit:
83+
click.secho("Skipping git commit.", fg="yellow")
84+
return
85+
86+
subprocess.call(["git", "add", ".ctf/config"], cwd=project_path)
87+
subprocess.call(["git", "commit", "-m", "init ctfcli project"], cwd=project_path)
88+
return
89+
90+
# Create a git repo in the project folder
91+
click.secho(f"Creating a git repo in {project_path}", fg="green")
92+
subprocess.call(["git", "init", str(project_path)])
93+
94+
if no_commit:
95+
click.secho("Skipping git commit.", fg="yellow")
96+
return
97+
98+
subprocess.call(["git", "add", ".ctf/config"], cwd=project_path)
99+
subprocess.call(["git", "commit", "-m", "init ctfcli project"], cwd=project_path)
66100

67101
def config(self):
68102
return COMMANDS.get("config")
@@ -81,21 +115,38 @@ def templates(self):
81115

82116

83117
COMMANDS = {
84-
"challenge": Challenge(),
85-
"config": Config(),
86-
"pages": Pages(),
87-
"plugins": Plugins(),
88-
"templates": Templates(),
118+
"challenge": ChallengeCommand(),
119+
"config": ConfigCommand(),
120+
"pages": PagesCommand(),
121+
"plugins": PluginsCommand(),
122+
"templates": TemplatesCommand(),
89123
"cli": CTFCLI(),
90124
}
91125

92126

93127
def main():
94-
# load plugins
128+
# Load plugins
95129
load_plugins(COMMANDS)
96130

97131
# Load CLI
98-
fire.Fire(CTFCLI)
132+
try:
133+
# if the command returns an int, then we serialize it as none to prevent fire from printing it
134+
# (this does not change the actual return value, so it's still good to use as an exit code)
135+
# everything else is returned as is, so fire can print help messages
136+
ret = fire.Fire(CTFCLI, serialize=lambda r: None if isinstance(r, int) else r)
137+
138+
if isinstance(ret, int):
139+
sys.exit(ret)
140+
141+
except ProjectNotInitialized:
142+
if click.confirm(
143+
"Outside of a ctfcli project, would you like to start a new project in this directory?",
144+
default=False,
145+
):
146+
CTFCLI.init()
147+
except KeyboardInterrupt:
148+
click.secho("\n[Ctrl-C] Aborting.", fg="red")
149+
sys.exit(2)
99150

100151

101152
if __name__ == "__main__":

0 commit comments

Comments
 (0)