This commit is contained in:
mdnapo 2024-05-04 20:44:48 +02:00
parent 81037f8ac5
commit 7a07cada25
17 changed files with 222 additions and 71 deletions

View File

@ -40,6 +40,8 @@ public partial class MycroForge
private async Task ExecuteAsync(string name)
{
_context.AssertDirectoryExists("api/routers");
var moduleName = name.Underscore();
await _context.CreateFile($"api/routers/{moduleName}.py", Template);

View File

@ -0,0 +1,66 @@
using System.CommandLine;
using Humanizer;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Generate
{
public class Service : Command, ISubCommandOf<Generate>
{
private static readonly Argument<string> NameArgument =
new(name: "name", description: "The name of the service");
private static readonly Option<bool> WithSession =
new(name: "--with-session", description: "Create a service that uses database sessions");
private static readonly string[] DefaultTemplate =
[
"class %class_name%:",
"",
"\tdef do_stuff(self, stuff: str) -> str:",
"\t\treturn f\"Hey, I'm doing stuff!\""
];
private static readonly string[] WithSessionTemplate =
[
"from orm.engine.async_session import async_session",
"# from orm.entities.user import User",
"",
"class %class_name%:",
"",
"\tasync def do_stuff(self, stuff: str) -> str:",
"\t\tasync with async_session() as session():",
"\t\t\t# stmt = select(User).where(User.firstname == \"John\")",
"\t\t\t# results = await session.scalars(stmt).all()",
"\t\t\t# print(len(results))",
"\t\t\tpass",
"\t\treturn f\"Hey, I'm doing stuff!\""
];
private readonly ProjectContext _context;
public Service(ProjectContext context) : base("service", "Generate a service")
{
_context = context;
AddAlias("s");
AddOption(WithSession);
this.SetHandler(ExecuteAsync, NameArgument, WithSession);
}
private async Task ExecuteAsync(string name, bool withSession)
{
_context.AssertDirectoryExists("services");
var className = Path.GetFileName(name).Pascalize();
var code = string.Join('\n', withSession ? WithSessionTemplate : DefaultTemplate)
.Replace("%class_name%", className);
await _context.CreateFile($"services/{name.Underscore().ToLower()}.py", code);
}
}
}
}

View File

@ -0,0 +1,23 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Generate
{
public class Venv : Command, ISubCommandOf<Generate>
{
private readonly ProjectContext _context;
public Venv(ProjectContext context) : base("venv", "Generate a venv")
{
_context = context;
this.SetHandler(ExecuteAsync);
}
private async Task ExecuteAsync() => await _context.Bash("python3 -m venv .venv");
}
}
}

View File

@ -0,0 +1,18 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Generate : Command, ISubCommandOf<MycroForge>
{
public Generate(IEnumerable<ISubCommandOf<Generate>> commands)
: base("generate", "Generate common items")
{
AddAlias("g");
foreach (var command in commands.Cast<Command>())
AddCommand(command);
}
}
}

View File

@ -6,6 +6,53 @@ 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>
{
private static readonly string[] DefaultFeatures =
@ -20,7 +67,9 @@ public partial class MycroForge
private static readonly Option<IEnumerable<string>> WithoutOption =
new Option<IEnumerable<string>>(name: "--without", description: "Features to exclude")
.FromAmong(DefaultFeatures);
{
AllowMultipleArgumentsPerToken = true
}.FromAmong(DefaultFeatures);
private readonly ProjectContext _context;
private readonly List<IFeature> _features;
@ -54,6 +103,9 @@ public partial class MycroForge
// 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")}");

View File

@ -43,6 +43,8 @@ public partial class MycroForge
private async Task ExecuteAsync(string name)
{
_context.AssertDirectoryExists("orm");
var className = name.Underscore().Pascalize();
var moduleName = name.Underscore();
var code = string.Join('\n', Template);

View File

@ -26,6 +26,8 @@ public partial class MycroForge
private async Task ExecuteAsync(string name)
{
_context.AssertDirectoryExists("orm/versions");
await _context.Bash(
"source .venv/bin/activate",
$"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")"

View File

@ -24,6 +24,11 @@ public static class ServiceCollectionExtensions
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Install>();
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Uninstall>();
// Register "m4g generate"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Generate>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Generate>, Commands.MycroForge.Generate.Service>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Generate>, Commands.MycroForge.Generate.Venv>();
// Register "m4g api"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Api>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Api>, Commands.MycroForge.Api.Run>();

View File

@ -0,0 +1,13 @@
using Humanizer;
namespace MycroForge.CLI.Extensions;
public static class StringExtensions
{
public static string NormalizePath(this string name)
{
var directoryPath = Path.GetDirectoryName(name).Underscore();
var filePath = Path.Join(directoryPath, name.Underscore().ToLower());
return filePath;
}
}

View File

@ -36,12 +36,6 @@ public sealed class Api : IFeature
{
var config = await context.LoadConfig();
if (config.Features.Contains(FeatureName))
{
Console.WriteLine($"Feature {FeatureName} has already been initialized.");
return;
}
await context.Bash(
"source .venv/bin/activate",
"python3 -m pip install fastapi uvicorn[standard]",
@ -54,7 +48,10 @@ public sealed class Api : IFeature
main = string.Join('\n', Main) + main;
await context.WriteFile("main.py", main);
config.Features.Add(FeatureName);
config.Api = new()
{
Port = 8000
};
await context.SaveConfig(config);
}

View File

@ -2,59 +2,11 @@ namespace MycroForge.CLI.Features;
public class Git : IFeature
{
#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 const string FeatureName = "git";
public string Name => FeatureName;
public async Task ExecuteAsync(ProjectContext context)
{
await context.CreateFile(".gitignore", GitIgnore);
await context.Bash($"git -c init.defaultBranch=main init {context.RootDirectory}");
}
}

View File

@ -60,12 +60,6 @@ public sealed class Orm : IFeature
{
var config = await context.LoadConfig();
if (config.Features.Contains(FeatureName))
{
Console.WriteLine($"Feature {FeatureName} has already been initialized.");
return;
}
await context.Bash(
"source .venv/bin/activate",
"python3 -m pip install asyncmy sqlalchemy alembic",
@ -86,8 +80,6 @@ public sealed class Orm : IFeature
await context.CreateFile("orm/entities/user.py", User);
config.Features.Add(FeatureName);
await context.SaveConfig(config);
}
}

View File

@ -15,7 +15,8 @@ using var host = Host
try
{
await host.Services.GetRequiredService<MycroForge.CLI.Commands.MycroForge>().InvokeAsync(args);
await host.Services.GetRequiredService<MycroForge.CLI.Commands.MycroForge>()
.InvokeAsync(args.Length == 0 ? ["--help"] : args);
}
catch(Exception e)
{

View File

@ -0,0 +1,9 @@
namespace MycroForge.CLI;
public partial class ProjectConfig
{
public class ApiConfig
{
public int Port { get; set; }
}
}

View File

@ -1,6 +1,6 @@
namespace MycroForge.CLI;
public class ProjectConfig
public partial class ProjectConfig
{
public List<string> Features { get; set; } = new();
public ApiConfig Api { get; set; } = default!;
}

View File

@ -32,6 +32,18 @@ public class ProjectContext
RootDirectory = path;
}
public void AssertDirectoryExists(string path)
{
var fullPath = Path.Combine(RootDirectory, path);
if (Directory.Exists(fullPath))
{
throw new(string.Join('\n',
$"{path} does not exist, make sure your in the correct directory."
));
}
}
public async Task CreateFile(string path, params string[] content)
{
var fullPath = Path.Combine(RootDirectory, path);

View File

@ -1,6 +1,11 @@
#!/usr/bin/bash
# Make sure to run this script form the MyrcoForge.CLI directory with sudo!
# Make sure to run this script from the MycroForge.CLI directory and prefixed with sudo!
# Example:
# sudo ./scripts/publish-linux.sh
dotnet publish --self-contained -r linux-x64
sudo cp bin/Release/net8.0/linux-x64 /usr/share/m4g
sudo ln -s /usr/share/m4g/MycroForge.CLI /usr/bin/local/m4g
sudo rm -rf /usr/share/m4g
sudo cp -r bin/Release/net8.0/linux-x64 /usr/share/m4g
sudo unlink /usr/local/bin/m4g
sudo ln -s /usr/share/m4g/MycroForge.CLI /usr/local/bin/m4g