- Refactored init & features

- Extended documentation
This commit is contained in:
2024-07-14 22:27:32 +02:00
parent 2d4bdbceeb
commit 02a82589ae
50 changed files with 16416 additions and 224 deletions

View File

@@ -0,0 +1,39 @@
using System.CommandLine;
using MycroForge.CLI.Features;
using MycroForge.Core;
using MycroForge.Core.Contract;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Add
{
public class Api : Command, ISubCommandOf<Add>
{
private static readonly Option<int> ApiPortOption = new(name: "--api-port", description: "The API port");
private readonly ProjectContext _context;
private readonly OptionsContainer _optionsContainer;
private readonly List<IFeature> _features;
public Api(ProjectContext context, OptionsContainer optionsContainer, IEnumerable<IFeature> features) :
base(Features.Api.FeatureName, "Add FastAPI to the project")
{
_context = context;
_optionsContainer = optionsContainer;
_features = features.ToList();
AddOption(ApiPortOption);
this.SetHandler(ExecuteAsync, ApiPortOption);
}
private async Task ExecuteAsync(int apiPort)
{
_optionsContainer.Set(new Features.Api.Options { ApiPort = apiPort });
var feature = _features.First(f => f.Name == Features.Api.FeatureName);
await feature.ExecuteAsync(_context);
}
}
}
}

View File

@@ -0,0 +1,48 @@
using System.CommandLine;
using MycroForge.CLI.Features;
using MycroForge.Core;
using MycroForge.Core.Contract;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Add
{
public class Db : Command, ISubCommandOf<Add>
{
private static readonly Option<int> DbhPortOption = new(
aliases: ["--database-host-port", "--dbh-port"],
description: "The database host port"
);
private static readonly Option<int> DbuPortOption = new(
aliases: ["--database-ui-port", "--dbu-port"],
description: "The database UI port"
);
private readonly ProjectContext _context;
private readonly OptionsContainer _optionsContainer;
private readonly List<IFeature> _features;
public Db(ProjectContext context, OptionsContainer optionsContainer, IEnumerable<IFeature> features) :
base(Features.Db.FeatureName, "Add SQLAlchemy & Alembic to the project")
{
_context = context;
_optionsContainer = optionsContainer;
_features = features.ToList();
AddOption(DbhPortOption);
AddOption(DbuPortOption);
this.SetHandler(ExecuteAsync, DbhPortOption, DbuPortOption);
}
private async Task ExecuteAsync(int dbhPort, int dbuPort)
{
_optionsContainer.Set(new Features.Db.Options { DbhPort = dbhPort, DbuPort = dbuPort });
var feature = _features.First(f => f.Name == Features.Db.FeatureName);
await feature.ExecuteAsync(_context);
}
}
}
}

View File

@@ -0,0 +1,32 @@
using System.CommandLine;
using MycroForge.CLI.Features;
using MycroForge.Core;
using MycroForge.Core.Contract;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Add
{
public class Git : Command, ISubCommandOf<Add>
{
private readonly ProjectContext _context;
private readonly List<IFeature> _features;
public Git(ProjectContext context, IEnumerable<IFeature> features) :
base(Features.Git.FeatureName, "Add git to the project")
{
_context = context;
_features = features.ToList();
this.SetHandler(ExecuteAsync);
}
private async Task ExecuteAsync()
{
var feature = _features.First(f => f.Name == Features.Git.FeatureName);
await feature.ExecuteAsync(_context);
}
}
}
}

View File

@@ -0,0 +1,32 @@
using System.CommandLine;
using MycroForge.CLI.Features;
using MycroForge.Core;
using MycroForge.Core.Contract;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Add
{
public class GitIgnore : Command, ISubCommandOf<Add>
{
private readonly ProjectContext _context;
private readonly List<IFeature> _features;
public GitIgnore(ProjectContext context, IEnumerable<IFeature> features) :
base(Features.GitIgnore.FeatureName, "Add a default .gitignore file to the project")
{
_context = context;
_features = features.ToList();
this.SetHandler(ExecuteAsync);
}
private async Task ExecuteAsync()
{
var feature = _features.First(f => f.Name == Features.GitIgnore.FeatureName);
await feature.ExecuteAsync(_context);
}
}
}
}

View File

@@ -0,0 +1,17 @@
using System.CommandLine;
using MycroForge.Core.Contract;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Add : Command, ISubCommandOf<MycroForge>
{
public Add(IEnumerable<ISubCommandOf<Add>> commands) :
base("add", "Add features to the project")
{
foreach (var command in commands.Cast<Command>())
AddCommand(command);
}
}
}

View File

@@ -8,12 +8,12 @@ public partial class MycroForge
{
public partial class Db
{
public partial class Run : Command, ISubCommandOf<Db>
public class Run : Command, ISubCommandOf<Db>
{
private readonly ProjectContext _context;
public Run(ProjectContext context) :
base("run", $"Runs {Features.Db.FeatureName}.docker-compose.yml")
base("run", $"Runs the services defined in {Features.Db.FeatureName}.docker-compose.yml")
{
_context = context;
this.SetHandler(ExecuteAsync);
@@ -22,7 +22,7 @@ public partial class MycroForge
private async Task ExecuteAsync()
{
var config = await _context.LoadConfig();
var env = $"DB_PORT={config.Db.DbPort} PMA_PORT={config.Db.PmaPort}";
var env = $"DBH_PORT={config.Db.DbhPort} DBU_PORT={config.Db.DbuPort}";
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml up -d");
}
}

View File

@@ -22,7 +22,7 @@ public partial class MycroForge
private async Task ExecuteAsync()
{
var config = await _context.LoadConfig();
var env = $"DB_PORT={config.Db.DbPort} PMA_PORT={config.Db.PmaPort}";
var env = $"DB_PORT={config.Db.DbhPort} PMA_PORT={config.Db.DbuPort}";
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml down");
}
}

View File

@@ -0,0 +1,21 @@
using System.CommandLine.Binding;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Init
{
public class Binder : BinderBase<Options>
{
protected override Options GetBoundValue(BindingContext ctx) => new()
{
Name = ctx.ParseResult.GetValueForArgument(NameArgument),
Without = ctx.ParseResult.GetValueForOption(WithoutOption),
ApiPort = ctx.ParseResult.GetValueForOption(ApiPortOption),
DbhPort = ctx.ParseResult.GetValueForOption(DbhPortOption),
DbuPort = ctx.ParseResult.GetValueForOption(DbuPortOption),
};
}
}
}

View File

@@ -0,0 +1,27 @@
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Init
{
public class Options
{
public string Name { get; set; } = string.Empty;
public IEnumerable<string>? Without { get; set; }
public int? ApiPort { get; set; }
public int? DbhPort { get; set; }
public int? DbuPort { get; set; }
public Features.Api.Options ApiOptions => new()
{
ApiPort = ApiPort <= 0 ? 8000 : ApiPort
};
public Features.Db.Options DbOptions => new()
{
DbhPort = DbhPort <= 0 ? 5050 : DbhPort,
DbuPort = DbuPort <= 0 ? 5051 : DbhPort
};
}
}
}

View File

@@ -7,58 +7,12 @@ namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
#region GitIgnore
private static readonly string[] GitIgnore =
[
"# Byte-compiled / optimized / DLL files", "__pycache__/", "*.py[cod]", "*$py.class", "# C extensions",
"*.so", "# Distribution / packaging", ".Python", "build/", "develop-eggs/", "dist/", "downloads/", "eggs/",
".eggs/", "lib/", "lib64/", "parts/", "sdist/", "var/", "wheels/", "share/python-wheels/", "*.egg-info/",
".installed.cfg", "*.egg", "MANIFEST", "# PyInstaller",
"# Usually these files are written by a python script from a template",
"# before PyInstaller builds the exe, so as to inject date/other infos into it.", "*.manifest", "*.spec",
"# Installer logs", "pip-log.txt", "pip-delete-this-directory.txt", "# Unit test / coverage reports",
"htmlcov/", ".tox/", ".nox/", ".coverage", ".coverage.*", ".cache", "nosetests.xml", "coverage.xml",
"*.cover", "*.py,cover", ".hypothesis/", ".pytest_cache/", "cover/", "# Translations", "*.mo", "*.pot",
"# Django stuff:", "*.log", "local_settings.py", "db.sqlite3", "db.sqlite3-journal", "# Flask stuff:",
"instance/", ".webassets-cache", "# Scrapy stuff:", ".scrapy", "# Sphinx documentation", "docs/_build/",
"# PyBuilder", ".pybuilder/", "target/", "# Jupyter Notebook", ".ipynb_checkpoints", "# IPython",
"profile_default/", "ipython_config.py", "# pyenv",
"# For a library or package, you might want to ignore these files since the code is",
"# intended to run in multiple environments; otherwise, check them in:", "# .python-version", "# pipenv",
"# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.",
"# However, in case of collaboration, if having platform-specific dependencies or dependencies",
"# having no cross-platform support, pipenv may install dependencies that don't work, or not",
"# install all needed dependencies.", "#Pipfile.lock", "# poetry",
"# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.",
"# This is especially recommended for binary packages to ensure reproducibility, and is more",
"# commonly ignored for libraries.",
"# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control",
"#poetry.lock", "# pdm",
"# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.",
"#pdm.lock",
"# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it",
"# in version control.", "# https://pdm.fming.dev/#use-with-ide", ".pdm.toml",
"# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm", "__pypackages__/",
"# Celery stuff", "celerybeat-schedule", "celerybeat.pid", "# SageMath parsed files", "*.sage.py",
"# Environments", ".env", ".venv", "env/", "venv/", "ENV/", "env.bak/", "venv.bak/",
"# Spyder project settings", ".spyderproject", ".spyproject", "# Rope project settings", ".ropeproject",
"# mkdocs documentation", "/site", "# mypy", ".mypy_cache/", ".dmypy.json", "dmypy.json",
"# Pyre type checker", ".pyre/", "# pytype static type analyzer", ".pytype/", "# Cython debug symbols",
"cython_debug/", "# PyCharm",
"# JetBrains specific template is maintained in a separate JetBrains.gitignore that can",
"# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore",
"# and can be added to the global gitignore or merged into this file. For a more nuclear",
"# option (not recommended) you can uncomment the following to ignore the entire idea folder.", "#.idea/"
];
#endregion
public class Init : Command, ISubCommandOf<MycroForge>
public partial class Init : Command, ISubCommandOf<MycroForge>
{
private static readonly string[] DefaultFeatures =
[
Features.Git.FeatureName,
Features.GitIgnore.FeatureName,
Features.Api.FeatureName,
Features.Db.FeatureName
];
@@ -72,44 +26,61 @@ public partial class MycroForge
AllowMultipleArgumentsPerToken = true
}.FromAmong(DefaultFeatures);
private static readonly Option<int> ApiPortOption =
new(name: "--api-port", description: "The API port");
private static readonly Option<int> DbhPortOption = new(
aliases: ["--database-host-port", "--dbh-port"],
description: "The database host port"
);
private static readonly Option<int> DbuPortOption = new(
aliases: ["--database-ui-port", "--dbu-port"],
description: "The database UI port"
);
private readonly ProjectContext _context;
private readonly List<IFeature> _features;
private readonly OptionsContainer _optionsContainer;
public Init(ProjectContext context, IEnumerable<IFeature> features) :
public Init(ProjectContext context, OptionsContainer optionsContainer, IEnumerable<IFeature> features) :
base("init", "Initialize a new project")
{
_context = context;
_optionsContainer = optionsContainer;
_features = features.ToList();
AddArgument(NameArgument);
AddOption(WithoutOption);
this.SetHandler(ExecuteAsync, NameArgument, WithoutOption);
AddOption(ApiPortOption);
AddOption(DbhPortOption);
AddOption(DbuPortOption);
this.SetHandler(ExecuteAsync, new Binder());
}
private async Task ExecuteAsync(string name, IEnumerable<string> without)
private async Task ExecuteAsync(Options options)
{
// Validate excluded features
var withoutList = without.ToList();
var withoutList = (options.Without ?? Enumerable.Empty<string>()).ToList();
foreach (var feature in withoutList)
if (_features.All(f => f.Name != feature))
throw new Exception($"Feature {feature} does not exist.");
// Create the project directory and change the directory for the ProjectContext
var projectRoot = await CreateDirectory(name);
var projectRoot = await CreateDirectory(options.Name);
_context.ChangeRootDirectory(projectRoot);
// Create the config file and initialize the config
await _context.CreateFile("m4g.json", "{}");
// Create the entrypoint file
await _context.CreateFile("main.py");
// Create the default .gitignore folder
await _context.CreateFile(".gitignore", GitIgnore);
// Create the venv
await _context.Bash($"python3 -m venv {Path.Combine(projectRoot, ".venv")}");
// Pass feature arguments to the ArgsContainer
_optionsContainer.Set(options.ApiOptions);
_optionsContainer.Set(options.DbOptions);
// Initialize default features
foreach (var feature in _features.Where(f => DefaultFeatures.Contains(f.Name)))
{

View File

@@ -24,9 +24,17 @@ public partial class MycroForge
private async Task ExecuteAsync(IEnumerable<string> packages)
{
var packs = packages.ToArray();
if (packs.Length == 0)
{
Console.WriteLine("m4g install requires at least one package.");
return;
}
await _context.Bash(
"source .venv/bin/activate",
$"pip install {string.Join(' ', packages)}",
$"pip install {string.Join(' ', packs)}",
"pip freeze > requirements.txt"
);
}

View File

@@ -0,0 +1,25 @@
namespace MycroForge.CLI.Commands;
public class OptionsContainer
{
private readonly Dictionary<Type, object> _args = new();
public void Set<T>(T? args)
{
if (args is null)
throw new ArgumentNullException();
_args[args.GetType()] = args;
}
public T Get<T>()
{
if (!_args.ContainsKey(typeof(T)))
throw new KeyNotFoundException();
if (_args[typeof(T)] is not T args)
throw new InvalidCastException();
return args;
}
}