Compare commits

...

10 Commits

Author SHA1 Message Date
9d1442b46b Supplemented commands documentation 2024-07-15 00:42:26 +02:00
be6b3691b4 Refactored FullyQualifiedName functionality 2024-07-14 23:08:47 +02:00
02a82589ae - Refactored init & features
- Extended documentation
2024-07-14 22:27:32 +02:00
2d4bdbceeb Cleaned up 2024-07-07 23:37:17 +02:00
9c3e2a25c0 Updated docs 2024-07-07 21:24:56 +02:00
3d96389f7f Cleaned up package build 2024-07-05 12:53:35 +02:00
ff9be76bf8 Added .gitkeep to templactes folder 2024-07-05 12:46:45 +02:00
1b6f0ee277 Cleaning up build process 2024-07-05 12:41:52 +02:00
7f67201bb2 Modified plugin system code 2024-07-05 12:35:36 +02:00
c220c214d2 Making plugin system more robust 2024-06-30 13:59:40 +02:00
110 changed files with 17323 additions and 346 deletions

View File

@ -20,7 +20,7 @@ public class CrudRouterGenerator
"",
"@router.get(\"/\")",
"async def list(",
"\tservice: Annotated[%service_class_name%, Depends(%service_class_name%)]",
"\tservice: Annotated[%service_class_name%, Depends()]",
"):",
"\ttry:",
"\t\tresult = await service.list()",
@ -32,7 +32,7 @@ public class CrudRouterGenerator
"@router.get(\"/{id}\")",
"async def get_by_id(",
"\tid: int,",
"\tservice: Annotated[%service_class_name%, Depends(%service_class_name%)]",
"\tservice: Annotated[%service_class_name%, Depends()]",
"):",
"\ttry:",
"\t\tresult = await service.get_by_id(id)",
@ -44,7 +44,7 @@ public class CrudRouterGenerator
"@router.post(\"/\")",
"async def create(",
"\trequest: Create%entity_class_name%Request,",
"\tservice: Annotated[%service_class_name%, Depends(%service_class_name%)]",
"\tservice: Annotated[%service_class_name%, Depends()]",
"):",
"\ttry:",
"\t\tawait service.create(request.model_dump())",
@ -57,7 +57,7 @@ public class CrudRouterGenerator
"async def update(",
"\tid: int,",
"\trequest: Update%entity_class_name%Request,",
"\tservice: Annotated[%service_class_name%, Depends(%service_class_name%)]",
"\tservice: Annotated[%service_class_name%, Depends()]",
"):",
"\ttry:",
"\t\tupdated = await service.update(id, request.model_dump(exclude_unset=True))",
@ -69,7 +69,7 @@ public class CrudRouterGenerator
"@router.delete(\"/{id}\")",
"async def delete(",
"\tid: int,",
"\tservice: Annotated[%service_class_name%, Depends(%service_class_name%)]",
"\tservice: Annotated[%service_class_name%, Depends()]",
"):",
"\ttry:",
"\t\tdeleted = await service.delete(id)",

View File

@ -1,4 +1,5 @@
using Humanizer;
using MycroForge.CLI.Commands;
using MycroForge.Core;
namespace MycroForge.CLI.CodeGen;
@ -143,16 +144,14 @@ public partial class EntityLinker
private async Task<EntityModel> LoadEntity(string name)
{
var fqn = new FullyQualifiedName(name);
var path = $"{Features.Db.FeatureName}/entities";
if (name.Split(':').Select(s => s.Trim()).ToArray() is { Length: 2 } fullName)
{
path = Path.Combine(path, fullName[0]);
name = fullName[1];
}
if (fqn.HasPath)
path = Path.Combine(path, fqn.Path);
path = Path.Combine(path, $"{name.Underscore().ToLower()}.py");
var entity = new EntityModel(name, path, await _context.ReadFile(path));
path = Path.Combine(path, $"{fqn.SnakeCasedName}.py");
var entity = new EntityModel(fqn.PascalizedName, path, await _context.ReadFile(path));
entity.Initialize();
return entity;
}

View File

@ -0,0 +1,28 @@
using Humanizer;
namespace MycroForge.CLI.Commands;
public class FullyQualifiedName
{
public string Path { get; }
public string PascalizedName { get; }
public string SnakeCasedName { get; }
public bool HasPath => Path.Length > 0;
public FullyQualifiedName(string name)
{
var path = string.Empty;
if (name.Split(':').Select(s => s.Trim()).ToArray() is { Length: 2 } fullName)
{
path = fullName[0];
name = fullName[1];
}
Path = path;
PascalizedName = name.Pascalize();
SnakeCasedName = name.Underscore().ToLower();
}
}

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

@ -28,17 +28,12 @@ public partial class MycroForge
private async Task ExecuteAsync(string entity)
{
var path = string.Empty;
if (entity.Split(':').Select(s => s.Trim()).ToArray() is { Length: 2 } fullName)
{
path = fullName[0];
entity = fullName[1];
}
var fqn = new FullyQualifiedName(entity);
await new CrudServiceGenerator(_context).Generate(path, entity);
await new RequestClassGenerator(_context).Generate(path, entity, RequestClassGenerator.Type.Create);
await new RequestClassGenerator(_context).Generate(path, entity, RequestClassGenerator.Type.Update);
await new CrudRouterGenerator(_context).Generate(path, entity);
await new CrudServiceGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Create);
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Update);
await new CrudRouterGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
}
}
}

View File

@ -1,5 +1,6 @@
using System.CommandLine;
using Humanizer;
using MycroForge.CLI.CodeGen;
using MycroForge.Core.Contract;
using MycroForge.CLI.Extensions;
using MycroForge.Core;
@ -42,30 +43,31 @@ public partial class MycroForge
private async Task ExecuteAsync(string name)
{
var fqn = new FullyQualifiedName(name);
var folderPath = $"{Features.Api.FeatureName}/routers";
_context.AssertDirectoryExists(folderPath);
if (name.FullyQualifiedName() is { Length: 2 } fullName)
{
folderPath = Path.Combine(folderPath, fullName[0]);
name = fullName[1];
}
var moduleImportPath = folderPath.Replace('\\', '.').Replace('/', '.');
var moduleName = name.Underscore().ToLower();
var fileName = $"{moduleName}.py";
if (fqn.HasPath)
folderPath = Path.Combine(folderPath, fqn.Path);
var fileName = $"{fqn.SnakeCasedName}.py";
var filePath = Path.Combine(folderPath, fileName);
await _context.CreateFile(filePath, Template);
var moduleImportPath = folderPath
.Replace('\\', '.')
.Replace('/', '.');
var main = await _context.ReadFile("main.py");
main += string.Join('\n',
$"\n\nfrom {moduleImportPath} import {moduleName}",
$"app.include_router(prefix=\"/{name.Kebaberize()}\", router={moduleName}.router)"
);
main = new MainModifier(main)
.Initialize()
.Import(from: moduleImportPath, import: fqn.SnakeCasedName)
.IncludeRouter(prefix: name.Kebaberize(), router: fqn.SnakeCasedName)
.Rewrite();
await _context.WriteFile("main.py", main);
}
}

View File

@ -69,28 +69,25 @@ public partial class MycroForge
private async Task ExecuteAsync(string name, IEnumerable<string> columns)
{
var fqn = new FullyQualifiedName(name);
var folderPath = $"{Features.Db.FeatureName}/entities";
_context.AssertDirectoryExists(Features.Db.FeatureName);
if (name.FullyQualifiedName() is { Length: 2 } fullName)
{
folderPath = Path.Combine(folderPath, fullName[0]);
name = fullName[1];
}
if (fqn.HasPath)
folderPath = Path.Combine(folderPath, fqn.Path);
var _columns = GetColumnDefinitions(columns.ToArray());
var className = name.Underscore().Pascalize();
var typeImports = string.Join(", ", _columns.Select(c => c.OrmType.Split('(').First()).Distinct());
var columnDefinitions = string.Join("\n\t", _columns.Select(ColumnToString));
var code = string.Join('\n', Template);
code = code.Replace("%type_imports%", typeImports);
code = code.Replace("%class_name%", className);
code = code.Replace("%table_name%", name.Underscore().ToLower().Pluralize());
code = code.Replace("%class_name%", fqn.PascalizedName);
code = code.Replace("%table_name%", fqn.SnakeCasedName.Pluralize());
code = code.Replace("%column_definitions%", columnDefinitions);
var fileName = $"{name.Underscore().ToLower()}.py";
var fileName = $"{fqn.SnakeCasedName}.py";
var filePath = Path.Combine(folderPath, fileName);
await _context.CreateFile(filePath, code);
@ -104,11 +101,11 @@ public partial class MycroForge
.ToLower();
var env = await _context.ReadFile($"{Features.Db.FeatureName}/env.py");
env = new DbEnvModifier(env, importPath, className).Rewrite();
env = new DbEnvModifier(env, importPath, fqn.PascalizedName).Rewrite();
await _context.WriteFile($"{Features.Db.FeatureName}/env.py", env);
var main = await _context.ReadFile("main.py");
main = new MainModifier(main).Initialize().Import(importPath, className).Rewrite();
main = new MainModifier(main).Initialize().Import(importPath, fqn.PascalizedName).Rewrite();
await _context.WriteFile("main.py", main);
}

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

@ -8,12 +8,12 @@ public partial class MycroForge
{
public partial class Db
{
public partial class Stop : Command, ISubCommandOf<Db>
public class Stop : Command, ISubCommandOf<Db>
{
private readonly ProjectContext _context;
public Stop(ProjectContext context) :
base("stop", $"Stops {Features.Db.FeatureName}.docker-compose.yml")
base("stop", $"Stops 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 = $"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

@ -57,18 +57,16 @@ public partial class MycroForge
private async Task ExecuteAsync(string name, bool withSession)
{
var fqn = new FullyQualifiedName(name);
var folderPath = string.Empty;
if (name.FullyQualifiedName() is { Length: 2} fullName)
{
folderPath = Path.Combine(folderPath, fullName[0]);
name = fullName[1];
}
if (fqn.HasPath)
folderPath = Path.Combine(folderPath, fqn.Path);
var filePath = Path.Combine(folderPath, $"{name.Underscore().ToLower()}.py");
var className = Path.GetFileName(name).Pascalize();
var code = string.Join('\n', withSession ? WithSessionTemplate : DefaultTemplate)
.Replace("%class_name%", className);
var filePath = Path.Combine(folderPath, $"{fqn.SnakeCasedName}.py");
var template = withSession ? WithSessionTemplate : DefaultTemplate;
var code = string.Join('\n', template)
.Replace("%class_name%", fqn.PascalizedName);
await _context.CreateFile(filePath, code);
}

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,31 @@
using System.CommandLine;
using MycroForge.Core;
using MycroForge.Core.Contract;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Plugin
{
public class Init : Command, ISubCommandOf<Plugin>
{
private static readonly Argument<string> NameArgument =
new(name: "name", description: "The name of your project");
private readonly ProjectContext _context;
public Init(ProjectContext context) : base("init", "Initialize a basic plugin project")
{
_context = context;
AddArgument(NameArgument);
this.SetHandler(ExecuteAsync, NameArgument);
}
private async Task ExecuteAsync(string name)
{
await _context.Bash($"dotnet new m4gp -n {name}");
}
}
}
}

View File

@ -19,7 +19,7 @@ public partial class MycroForge
private void ExecuteAsync()
{
foreach (var plugin in Plugins.Loaded)
Console.WriteLine($"name: {plugin.Name}, command: {plugin.Command}");
Console.WriteLine($"{plugin.Name}");
}
}
}

View File

@ -1,11 +1,10 @@
using System.CommandLine;
using MycroForge.Core.Contract;
using Core_RootCommand = MycroForge.Core.RootCommand;
using RootCommand = MycroForge.Core.RootCommand;
namespace MycroForge.CLI.Commands;
public sealed partial class MycroForge : Core_RootCommand
public sealed partial class MycroForge : RootCommand
{
public override string Name => "m4g";

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;
}
}

View File

@ -10,11 +10,13 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection RegisterCommandDefaults(this IServiceCollection services)
{
// Register ProjectContext & features
// Register ProjectContext, OptionsContainer & features
services.AddScoped<ProjectContext>();
services.AddScoped<IFeature, Git>();
services.AddScoped<OptionsContainer>();
services.AddScoped<IFeature, Api>();
services.AddScoped<IFeature, Db>();
services.AddScoped<IFeature, Git>();
services.AddScoped<IFeature, GitIgnore>();
// Register "m4g"
services.AddScoped<Commands.MycroForge>();
@ -22,6 +24,13 @@ public static class ServiceCollectionExtensions
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Install>();
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Uninstall>();
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Hydrate>();
// Register "m4g add"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Add>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Add>, Commands.MycroForge.Add.Api>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Add>, Commands.MycroForge.Add.Db>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Add>, Commands.MycroForge.Add.Git>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Add>, Commands.MycroForge.Add.GitIgnore>();
// Register "m4g generate"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Generate>();
@ -50,6 +59,7 @@ public static class ServiceCollectionExtensions
// Register "m4g plugin"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Plugin>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Plugin>, Commands.MycroForge.Plugin.Init>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Plugin>, Commands.MycroForge.Plugin.List>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Plugin>, Commands.MycroForge.Plugin.Install>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Plugin>, Commands.MycroForge.Plugin.Uninstall>();

View File

@ -2,11 +2,6 @@
public static class StringExtensions
{
public static string[] FullyQualifiedName(this string name)
{
return name.Split(':').Select(s => s.Trim()).ToArray();
}
public static string DeduplicateDots(this string path)
{
while (path.Contains(".."))

View File

@ -0,0 +1,9 @@
namespace MycroForge.CLI.Features;
public sealed partial class Api
{
public class Options
{
public int? ApiPort { get; set; }
}
}

View File

@ -1,10 +1,11 @@
using MycroForge.Core;
using MycroForge.CLI.Commands;
using MycroForge.Core;
namespace MycroForge.CLI.Features;
public sealed class Api : IFeature
public sealed partial class Api : IFeature
{
#region Main
#region Templates
private static readonly string[] RouterTemplate =
[
@ -24,7 +25,6 @@ public sealed class Api : IFeature
"from fastapi import FastAPI",
$"from {FeatureName}.routers import hello",
"",
"",
"app = FastAPI()",
"",
"app.include_router(prefix=\"/hello\", router=hello.router)"
@ -36,15 +36,23 @@ public sealed class Api : IFeature
public string Name => FeatureName;
private readonly OptionsContainer _optionsContainer;
public Api(OptionsContainer optionsContainer)
{
_optionsContainer = optionsContainer;
}
public async Task ExecuteAsync(ProjectContext context)
{
var config = await context.LoadConfig();
config.Api = new() { Port = 8000 };
var options = _optionsContainer.Get<Options>();
var config = await context.LoadConfig(create: true);
config.Api = new() { Port = options.ApiPort ?? 8000 };
await context.SaveConfig(config);
await context.Bash(
"source .venv/bin/activate",
"python3 -m pip install fastapi uvicorn[standard]",
"python3 -m pip install fastapi",
"python3 -m pip freeze > requirements.txt"
);

View File

@ -0,0 +1,10 @@
namespace MycroForge.CLI.Features;
public sealed partial class Db
{
public class Options
{
public int? DbhPort { get; set; }
public int? DbuPort { get; set; }
}
}

View File

@ -1,9 +1,10 @@
using MycroForge.CLI.CodeGen;
using MycroForge.CLI.Commands;
using MycroForge.Core;
namespace MycroForge.CLI.Features;
public sealed class Db : IFeature
public sealed partial class Db : IFeature
{
#region Defaults
@ -37,7 +38,7 @@ public sealed class Db : IFeature
private static readonly string[] DockerCompose =
[
"version: '3.8'",
"# Access the database UI at http://localhost:${DB_PORT}.",
"# Access the database UI at http://localhost:${DBU_PORT}.",
"# Login: username = root & password = password",
"",
"services:",
@ -47,7 +48,7 @@ public sealed class Db : IFeature
" networks:",
" - default",
" ports:",
" - '${DB_PORT}:3306'",
" - '${DBH_PORT}:3306'",
" environment:",
" MYSQL_ROOT_PASSWORD: 'password'",
" MYSQL_USER: 'root'",
@ -60,7 +61,7 @@ public sealed class Db : IFeature
" image: phpmyadmin/phpmyadmin",
" container_name: %app_name%_phpmyadmin",
" ports:",
" - '${PMA_PORT}:80'",
" - '${DBU_PORT}:80'",
" networks:",
" - default",
" environment:",
@ -78,12 +79,24 @@ public sealed class Db : IFeature
public const string FeatureName = "db";
private readonly OptionsContainer _optionsContainer;
public string Name => FeatureName;
public Db(OptionsContainer optionsContainer)
{
_optionsContainer = optionsContainer;
}
public async Task ExecuteAsync(ProjectContext context)
{
var config = await context.LoadConfig();
config.Db = new() { DbPort = 5050, PmaPort = 5051 };
var options = _optionsContainer.Get<Options>();
var config = await context.LoadConfig(create: true);
config.Db = new()
{
DbhPort = options.DbhPort ?? 5050,
DbuPort = options.DbuPort ?? 5051
};
await context.SaveConfig(config);
var appName = context.AppName;
@ -101,7 +114,7 @@ public sealed class Db : IFeature
var settings = string.Join('\n', Settings)
.Replace("%app_name%", appName)
.Replace("%db_port%", config.Db.DbPort.ToString())
.Replace("%db_port%", config.Db.DbhPort.ToString())
;
await context.CreateFile($"{FeatureName}/settings.py", settings);

View File

@ -0,0 +1,61 @@
using MycroForge.Core;
namespace MycroForge.CLI.Features;
public class GitIgnore : IFeature
{
#region GitIgnore
private static readonly string[] GitIgnoreTemplate =
[
"# 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 = "gitignore";
public string Name => FeatureName;
public async Task ExecuteAsync(ProjectContext context)
{
await context.CreateFile(".gitignore", GitIgnoreTemplate);
}
}

View File

@ -0,0 +1,5 @@
#!/usr/bin/bash
dotnet pack -v d
dotnet nuget push nupkg/MycroForge.CLI.1.0.0.nupkg --source devdisciples

View File

@ -5,7 +5,6 @@ namespace MycroForge.Core.Contract;
public interface ICommandPlugin
{
public string? Name { get; }
public string Command { get; }
public void RegisterServices(IServiceCollection services);
}

View File

@ -4,6 +4,12 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>MycroForge.Core</PackageId>
<Description>The MycroForge core package</Description>
<Version>1.0.0</Version>
<Authors>Donné Napo</Authors>
<Company>Dev Disciples</Company>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>

View File

@ -4,7 +4,7 @@ public partial class ProjectConfig
{
public class DbConfig
{
public int DbPort { get; set; }
public int PmaPort { get; set; }
public int DbhPort { get; set; }
public int DbuPort { get; set; }
}
}

View File

@ -12,10 +12,13 @@ public class ProjectContext
private string ConfigPath => Path.Combine(RootDirectory, "m4g.json");
public async Task<ProjectConfig> LoadConfig()
public async Task<ProjectConfig> LoadConfig(bool create = false)
{
if (!File.Exists(ConfigPath))
throw new FileNotFoundException($"File {ConfigPath} does not exist.");
{
if (create) await CreateFile("m4g.json", "{}");
else throw new FileNotFoundException($"File {ConfigPath} does not exist.");
}
var config = await JsonSerializer.DeserializeAsync<ProjectConfig>(
File.OpenRead(ConfigPath),

View File

@ -0,0 +1,6 @@
### Publish to local nuget repo
```shell
dotnet build -r Release
dotnet nuget push --source devdisciples bin/Release/MycroForge.Core.1.0.0.nupkg
```

View File

@ -0,0 +1,5 @@
bin/
obj/
templates/
!templates/.gitkeep
MycroForge.Package.PluginTemplate.sln

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- The package metadata. Fill in the properties marked as TODO below -->
<!-- Follow the instructions on https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices -->
<PackageId>MycroForge.PluginTemplate.Package</PackageId>
<PackageVersion>1.0</PackageVersion>
<Title>A template for generating MycroForge plugins</Title>
<Authors>Donné Napo</Authors>
<Description>Template to use when creating a plugin for the MycroForge CLI.</Description>
<PackageTags>dotnet-new;templates;</PackageTags>
<PackageProjectUrl>https://git.devdisciples.com/devdisciples/mycroforge</PackageProjectUrl>
<PackageType>Template</PackageType>
<TargetFramework>net8.0</TargetFramework>
<IncludeContentInPack>true</IncludeContentInPack>
<IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders>
<NoWarn>$(NoWarn);NU5128</NoWarn>
<NoDefaultExcludes>true</NoDefaultExcludes>
</PropertyGroup>
<ItemGroup>
<Content Include="templates\**\*" Exclude="templates\**\bin\**;templates\**\obj\**"/>
<Compile Remove="**\*"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
### Used resources
https://www.youtube.com/watch?v=XzD-95qfWJM
https://www.youtube.com/watch?v=rdWZo5PD9Ek
https://learn.microsoft.com/en-us/dotnet/core/tutorials/cli-templates-create-template-package?pivots=dotnet-8-0
### Build the package
`dotnet pack`
### Push to local nuget
`dotnet nuget push bin/Release/MycroForge.PluginTemplate.Package.1.0.0.nupkg --source devdisciples`
### Install template package from local nuget
`dotnet new install MycroForge.PluginTemplate.Package --nuget-source devdisciples`

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
rm -rf templates/MycroForge.PluginTemplate
cp -R ../MycroForge.PluginTemplate templates/MycroForge.PluginTemplate
dotnet pack

View File

@ -0,0 +1,21 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Donné Napo",
"defaultName": "My.Plugin",
"name": "MycroForge plugin template",
"description": "Creates a basic MycroForge plugin project",
"projectURL": "https://github.com/mdnapo/mycroforge",
"repository": {
"url": "https://github.com/",
"type": "GitHub"
},
"classifications": ["Console","Plugin"],
"identity": "MycroForge.PluginTemplate",
"shortName": "m4gp",
"sourceName": "MycroForge.PluginTemplate",
"tags": {
"language": "C#",
"type": "project"
},
"preferNameDirectory": true
}

View File

@ -0,0 +1,36 @@
using System.CommandLine;
using MycroForge.Core;
using MycroForge.Core.Contract;
using RootCommand = MycroForge.Core.RootCommand;
namespace MycroForge.PluginTemplate;
public class HelloWorldCommand : Command, ISubCommandOf<RootCommand>
{
private readonly Argument<string> NameArgument =
new(name: "name", description: "The name of the person to greet");
private readonly Option<bool> AllCapsOption =
new(aliases: ["-a", "--all-caps"], description: "Print the name in all caps");
private readonly ProjectContext _context;
public HelloWorldCommand(ProjectContext context) :
base("hello", "An example command generated by dotnet new using the m4gp template")
{
_context = context;
AddArgument(NameArgument);
AddOption(AllCapsOption);
this.SetHandler(ExecuteAsync, NameArgument, AllCapsOption);
}
private async Task ExecuteAsync(string name, bool allCaps)
{
name = allCaps ? name.ToUpper() : name;
await _context.CreateFile("hello_world.txt",
$"Hello {name}!",
"This file was generated by your custom command!"
);
}
}

View File

@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using MycroForge.Core.Contract;
using RootCommand = MycroForge.Core.RootCommand;
namespace MycroForge.PluginTemplate;
public class HelloWorldCommandPlugin : ICommandPlugin
{
public string Name => "MycroForge.PluginTemplate";
public void RegisterServices(IServiceCollection services)
{
services.AddScoped<ISubCommandOf<RootCommand>, HelloWorldCommand>();
}
}

View File

@ -7,7 +7,11 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MycroForge.Core\MycroForge.Core.csproj" />
<Content Include=".template.config\template.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MycroForge.Core" Version="1.0.0" />
</ItemGroup>
</Project>

View File

@ -1,6 +0,0 @@
namespace MycroForge.TestPlugin;
public static class Constants
{
public const string MainCommandName = "mj";
}

View File

@ -1,25 +0,0 @@
using System.CommandLine;
using MycroForge.Core;
using MycroForge.Core.Contract;
using RootCommand = MycroForge.Core.RootCommand;
namespace MycroForge.TestPlugin;
public class MyJewelleryCommand : Command, ISubCommandOf<RootCommand>
{
private readonly ProjectContext _context;
public MyJewelleryCommand(ProjectContext context) :
base(Constants.MainCommandName, "Custom command for My Jewellery specific stuff")
{
_context = context;
this.SetHandler(ExecuteAsync);
}
private async Task ExecuteAsync()
{
await _context.CreateFile("hello_world.txt",
"My Jewellery command plugin is working!"
);
}
}

View File

@ -1,16 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using MycroForge.Core.Contract;
using RootCommand = MycroForge.Core.RootCommand;
namespace MycroForge.TestPlugin;
public class MyJewelleryCommandPlugin : ICommandPlugin
{
public string Name => $"{nameof(MycroForge)}.{nameof(TestPlugin)}";
public string Command => Constants.MainCommandName;
public void RegisterServices(IServiceCollection services)
{
services.AddScoped<ISubCommandOf<RootCommand>, MyJewelleryCommand>();
}
}

View File

@ -4,7 +4,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.CLI", "MycroForg
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.Core", "MycroForge.Core\MycroForge.Core.csproj", "{CFF8BD4E-520D-4319-BA80-3F49B5F493BA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.TestPlugin", "MycroForge.TestPlugin\MycroForge.TestPlugin.csproj", "{7C479E68-98FA-4FBC-B5E4-7116015774B3}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate", "MycroForge.PluginTemplate\MycroForge.PluginTemplate.csproj", "{114A2B34-D77E-42AE-ADAF-0CD68C7B8D32}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate.Package", "MycroForge.PluginTemplate.Package\MycroForge.PluginTemplate.Package.csproj", "{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -20,9 +22,13 @@ Global
{CFF8BD4E-520D-4319-BA80-3F49B5F493BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CFF8BD4E-520D-4319-BA80-3F49B5F493BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CFF8BD4E-520D-4319-BA80-3F49B5F493BA}.Release|Any CPU.Build.0 = Release|Any CPU
{7C479E68-98FA-4FBC-B5E4-7116015774B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C479E68-98FA-4FBC-B5E4-7116015774B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C479E68-98FA-4FBC-B5E4-7116015774B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C479E68-98FA-4FBC-B5E4-7116015774B3}.Release|Any CPU.Build.0 = Release|Any CPU
{114A2B34-D77E-42AE-ADAF-0CD68C7B8D32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{114A2B34-D77E-42AE-ADAF-0CD68C7B8D32}.Debug|Any CPU.Build.0 = Debug|Any CPU
{114A2B34-D77E-42AE-ADAF-0CD68C7B8D32}.Release|Any CPU.ActiveCfg = Release|Any CPU
{114A2B34-D77E-42AE-ADAF-0CD68C7B8D32}.Release|Any CPU.Build.0 = Release|Any CPU
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -21,3 +21,9 @@ The MycroForge CLI assumes a linux compatible environment, so on Windows you'll
Run the install script in the same directory as the downloaded zip. See the example below for linux-x64.
`sudo ./install.sh m4g-<platform>.zip <platform>`
### Add DevDisciples NuGet source
```bash
dotnet nuget add source --name devdisciples --username username --password password https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json --store-password-in-clear-text
```

20
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,141 +1,41 @@
# General introduction
## What is MycroForge?
MycroForge is an opinionated CLI tool that is meant to facilitate the development of FastAPI & SQLAlchemy based backends.
It provides a command line interface that allows users to generate a skeleton project and other common items like
database entities, migrations, routers and even basic CRUD functionality. The main purpose of this tool is to generate
boilerplate code and to provide a unified interface for performing recurrent development activities.
## Generating a project
To generate a project you can run the following command.
`m4g init <name>`
```
Description:
Initialize a new project
Usage:
m4g init <name> [options]
Arguments:
<name> The name of your project
Options:
--without <api|db|git> Features to exclude
-?, -h, --help Show help and usage information
```
Running this command will generate the following project structure.
```
📦<project_name>
┣ 📂.git
┣ 📂.venv
┣ 📂api
┃ ┗ 📂routers
┃ ┃ ┗ 📜hello.py
┣ 📂db
┃ ┣ 📂engine
┃ ┃ ┗ 📜async_session.py
┃ ┣ 📂entities
┃ ┃ ┗ 📜entity_base.py
┃ ┣ 📂versions
┃ ┣ 📜README
┃ ┣ 📜env.py
┃ ┣ 📜script.py.mako
┃ ┗ 📜settings.py
┣ 📜.gitignore
┣ 📜alembic.ini
┣ 📜db.docker-compose.yml
┣ 📜m4g.json
┣ 📜main.py
┗ 📜requirements.txt
```
Let's go through these one by one.
### .git
The `m4g init` command will initialize new projects with git by default.
If you don't want to use git you can pass the option `--without git` to `m4g init`.
### .venv
To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
TODO: This is a good section to introduce the `m4g hydrate` command.
### api/routers/hello.py
This file defines a basic example router, which is imported and mapped in `main.py`. This router is just an example and
can be removed or modified at you discretion.
### db/engine/async_session.py
This file defines the `async_session` function, which can be used to open an asynchronous session to a database.
### db/entities/entity_base.py
This file contains an automatically generated entity base class that derives from the DeclarativeBase.
All entities must inherit from this class, so that SQLAlchemy & alembic can track them. The entities directory is also
where all newly generated entities will be stored.
### db/versions
This is where the generated database migrations will be stored.
### db/README
This README file is automatically generated by the alembic init command.
### db/env.py
This is the database environment file that is used by alembic to interact with the database.
If you take a closer look at the imports, you'll see that the file has been modified to assign `EntityBase.metadata` to
a variable called `target_metadata`, this will allow alembic to track changes in your entities. You'll also find that
the `DbSettings` class is used to get the connectionstring. Any time you generate a new database entity, or create a
many-to-many relation between two entities, this file will also be modified to include the generated classes.
### db/script.py.mako
This file is automatically generated by the alembic init command.
### db/settings.py
This file defines the `DbSettings` class, that is responsible for retrieving the database connectionstring.
You will probably want to modify this class to retrieve the connectionstring from a secret manager at some point.
### .gitignore
The default .gitignore file that is generated by the `m4g init` command. Modify this file at your discretion.
### alembic.ini
This file is automatically generated by the alembic init command.
### db.docker-compose.yml
A docker compose file for running a database locally.
### m4g.json
This file contains some configs that are used by the CLI, for example the ports to map to the API and database.
### main.py
The entrypoint for the application. When generating entities, many-to-many relations or routers, this file will be
modified to include the generated files.
### requirements.txt
The requirements file containing the Python dependencies.
TODO: introduce the `m4g install` & `m4g uninstall` commands.
## Scripting
TODO: Dedicate a section to scripting
# Website
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

3
docs/babel.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

View File

@ -0,0 +1,24 @@
# Commands
```
Description:
The MycroForge CLI tool.
Usage:
m4g [command] [options]
Options:
--version Show version information
-?, -h, --help Show help and usage information
Commands:
init <name> Initialize a new project
i, install <packages> Install packages and update the requirements.txt
u, uninstall <packages> Uninstall packages and update the requirements.txt
hydrate Initialize venv and install dependencies from requirements.txt
add Add features to the project
g, generate Generate common items
api API related commands
db Database related commands
p, plugin Plugin related commands
```

View File

@ -0,0 +1,22 @@
---
sidebar_position: 1
---
# m4g add
```
Description:
Add features to the project
Usage:
m4g add [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
api Add FastAPI to the project
db Add SQLAlchemy & Alembic to the project
git Add git to the project
gitignore Add a default .gitignore file to the project
```

View File

@ -0,0 +1,17 @@
---
sidebar_position: 1
---
# m4g add api
```
Description:
Add FastAPI to the project
Usage:
m4g add api [options]
Options:
--api-port <api-port> The API port
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,18 @@
---
sidebar_position: 1
---
# m4g add db
```
Description:
Add SQLAlchemy & Alembic to the project
Usage:
m4g add db [options]
Options:
--database-host-port, --dbh-port <database-host-port> The database host port
--database-ui-port, --dbu-port <database-ui-port> The database UI port
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g add git
```
Description:
Add git to the project
Usage:
m4g add git [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g add gitignore
```
Description:
Add a default .gitignore file to the project
Usage:
m4g add gitignore [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g api
```
Description:
API related commands
Usage:
m4g api [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
run Run your app
g, generate Generate an API item
```

View File

@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g api generate
```
Description:
Generate an API item
Usage:
m4g api generate [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
r, router <name> Generate an api router
crud <entity> Generated CRUD functionality for an entity
```

View File

@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g api generate crud
```
Description:
Generated CRUD functionality for an entity
Usage:
m4g api generate crud <entity> [options]
Arguments:
<entity> The entity to target
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g api generate router
```
Description:
Generate an api router
Usage:
m4g api generate router <name> [options]
Arguments:
<name> The name of the api router
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g api run
```
Description:
Run your app
Usage:
m4g api run [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,24 @@
---
sidebar_position: 1
---
# m4g db
```
Description:
Database related commands
Usage:
m4g db [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
run Runs the services defined in db.docker-compose.yml
stop Stops db.docker-compose.yml
migrate Apply migrations to the database
rollback Rollback the last migration
g, generate Generate a database item
link Define relationships between entities
```

View File

@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g db generate
```
Description:
Generate a database item
Usage:
m4g db generate [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
e, entity <name> Generate and database entity
m, migration <name> Generate a migration
```

View File

@ -0,0 +1,35 @@
---
sidebar_position: 1
---
# m4g db generate entity
```
Description:
Generate and database entity
Usage:
m4g db generate entity <name> [options]
Arguments:
<name> The name of the database entity
Supported formats:
Entity
path/relative/to/entities:Entity
Options:
-c, --column <column> Specify the fields to add.
Format:
<name>:<native_type>:<orm_type>
<name> = Name of the column
<native_type> = The native Python type
<orm_type> = The SQLAlchemy type
Example:
first_name:str:String(255)
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g db generate migration
```
Description:
Generate a migration
Usage:
m4g db generate migration <name> [options]
Arguments:
<name> The name of the migration
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,21 @@
---
sidebar_position: 1
---
# m4g db link
```
Description:
Define relationships between entities
Usage:
m4g db link [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
one <entity> Define a 1:n relation
many <entity> Define a n:m relation
```

View File

@ -0,0 +1,21 @@
---
sidebar_position: 1
---
# m4g db link many
```
Description:
Define a n:m relation
Usage:
m4g db link many <entity> [options]
Arguments:
<entity> The left side of the relation
Options:
--to-one <to-one> The right side of the relation
--to-many <to-many> The right side of the relation
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,21 @@
---
sidebar_position: 1
---
# m4g db link one
```
Description:
Define a 1:n relation
Usage:
m4g db link one <entity> [options]
Arguments:
<entity> The left side of the relation
Options:
--to-one <to-one> The right side of the relation
--to-many <to-many> The right side of the relation
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g db migrate
```
Description:
Apply migrations to the database
Usage:
m4g db migrate [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g db rollback
```
Description:
Rollback the last migration
Usage:
m4g db rollback [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g db run
```
Description:
Runs the services defined in db.docker-compose.yml
Usage:
m4g db run [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g db stop
```
Description:
Stops db.docker-compose.yml
Usage:
m4g db stop [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g generate
```
Description:
Generate common items
Usage:
m4g generate [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
s, service <name> Generate a service
venv Generate a venv
```

View File

@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g generate service
```
Description:
Generate a service
Usage:
m4g generate service <name> [options]
Arguments:
<name> The name of the service
Options:
--with-session Create a service that uses database sessions
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g generate venv
```
Description:
Generate a venv
Usage:
m4g generate venv [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g hydrate
```
Description:
Initialize venv and install dependencies from requirements.txt
Usage:
m4g hydrate [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,23 @@
---
sidebar_position: 1
---
# m4g init
```
Description:
Initialize a new project
Usage:
m4g init <name> [options]
Arguments:
<name> The name of your project
Options:
--without <api|db|git|gitignore> Features to exclude
--api-port <api-port> The API port
--database-host-port, --dbh-port <database-host-port> The database host port
--database-ui-port, --dbu-port <database-ui-port> The database UI port
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g install
```
Description:
Install packages and update the requirements.txt
Usage:
m4g install [<packages>...] [options]
Arguments:
<packages> The names of the packages to install
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,22 @@
---
sidebar_position: 1
---
# m4g plugin
```
Description:
Plugin related commands
Usage:
m4g plugin [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
init <name> Initialize a basic plugin project
l, list, ls List all installed plugins
i, install Install a plugin
u, uninstall <name> Uninstall a plugin
```

View File

@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g plugin init
```
Description:
Initialize a basic plugin project
Usage:
m4g plugin init <name> [options]
Arguments:
<name> The name of your project
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,17 @@
---
sidebar_position: 1
---
# m4g plugin install
```
Description:
Install a plugin
Usage:
m4g plugin install [options]
Options:
-p, --platform <linux_arm|linux_arm64|linux_x64|osx_arm64|osx_x64> (REQUIRED) The platform to target when building the plugin
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g plugin list
```
Description:
List all installed plugins
Usage:
m4g plugin list [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g plugin uninstall
```
Description:
Uninstall a plugin
Usage:
m4g plugin uninstall [<name>...] [options]
Arguments:
<name> The names of the plugins you want to uninstall
Options:
-?, -h, --help Show help and usage information
```

View File

@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g uninstall
```
Description:
Uninstall packages and update the requirements.txt
Usage:
m4g uninstall [<packages>...] [options]
Arguments:
<packages> The names of the packages to uninstall
Options:
-y, --yes Dont ask for confirmation of uninstall deletions
-?, -h, --help Show help and usage information
```

20
docs/docs/install.md Normal file
View File

@ -0,0 +1,20 @@
---
sidebar_position: 2
---
# Install
## Requirements
MycroForge has the following dependencies.
- bash
- git
- Python3 (3.10)
- Docker
### Windows
To simplify the implementation of this tool, it assumes that it's running in a POSIX compliant environment.
MycroForge has been developed and tested on Windows in WSL2 Ubuntu 22.04.03.
So when running on Windows, it's recommended to run MycroForge in the same environment or atleast in a similar WSL2 distro.

145
docs/docs/intro.md Normal file
View File

@ -0,0 +1,145 @@
---
sidebar_position: 1
---
# Intro
## What is MycroForge?
MycroForge is an opinionated CLI tool that is meant to facilitate the development of FastAPI & SQLAlchemy based backends.
It provides a command line interface that allows users to generate a skeleton project and other common items like
database entities, migrations, routers and even basic CRUD functionality. The main purpose of this tool is to generate
boilerplate code and to provide a unified interface for performing recurrent development activities.
## Generating a project
To generate a project you can run the following command.
`m4g init <name>`
```
Description:
Initialize a new project
Usage:
m4g init <name> [options]
Arguments:
<name> The name of your project
Options:
--without <api|db|git> Features to exclude
-?, -h, --help Show help and usage information
```
Running this command will generate the following project structure.
```
📦<project_name>
┣ 📂.git
┣ 📂.venv
┣ 📂api
┃ ┗ 📂routers
┃ ┃ ┗ 📜hello.py
┣ 📂db
┃ ┣ 📂engine
┃ ┃ ┗ 📜async_session.py
┃ ┣ 📂entities
┃ ┃ ┗ 📜entity_base.py
┃ ┣ 📂versions
┃ ┣ 📜README
┃ ┣ 📜env.py
┃ ┣ 📜script.py.mako
┃ ┗ 📜settings.py
┣ 📜.gitignore
┣ 📜alembic.ini
┣ 📜db.docker-compose.yml
┣ 📜m4g.json
┣ 📜main.py
┗ 📜requirements.txt
```
Let's go through these one by one.
### .git
The `m4g init` command will initialize new projects with git by default.
If you don't want to use git you can pass the option `--without git` to `m4g init`.
### .venv
To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
TODO: This is a good section to introduce the `m4g hydrate` command.
### api/routers/hello.py
This file defines a basic example router, which is imported and mapped in `main.py`. This router is just an example and
can be removed or modified at you discretion.
### db/engine/async_session.py
This file defines the `async_session` function, which can be used to open an asynchronous session to a database.
### db/entities/entity_base.py
This file contains an automatically generated entity base class that derives from the DeclarativeBase.
All entities must inherit from this class, so that SQLAlchemy & alembic can track them. The entities directory is also
where all newly generated entities will be stored.
### db/versions
This is where the generated database migrations will be stored.
### db/README
This README file is automatically generated by the alembic init command.
### db/env.py
This is the database environment file that is used by alembic to interact with the database.
If you take a closer look at the imports, you'll see that the file has been modified to assign `EntityBase.metadata` to
a variable called `target_metadata`, this will allow alembic to track changes in your entities. You'll also find that
the `DbSettings` class is used to get the connectionstring. Any time you generate a new database entity, or create a
many-to-many relation between two entities, this file will also be modified to include the generated classes.
### db/script.py.mako
This file is automatically generated by the alembic init command.
### db/settings.py
This file defines the `DbSettings` class, that is responsible for retrieving the database connectionstring.
You will probably want to modify this class to retrieve the connectionstring from a secret manager at some point.
### .gitignore
The default .gitignore file that is generated by the `m4g init` command. Modify this file at your discretion.
### alembic.ini
This file is automatically generated by the alembic init command.
### db.docker-compose.yml
A docker compose file for running a database locally.
### m4g.json
This file contains some configs that are used by the CLI, for example the ports to map to the API and database.
### main.py
The entrypoint for the application. When generating entities, many-to-many relations or routers, this file will be
modified to include the generated files.
### requirements.txt
The requirements file containing the Python dependencies.
TODO: introduce the `m4g install` & `m4g uninstall` commands.
## Plugin system
TODO: Dedicate a section to the Plugin system

283
docs/docs/tutorial.md Normal file
View File

@ -0,0 +1,283 @@
---
sidebar_position: 3
---
# Tutorial
We're going to build a simple todo app to demonstrate the capabilities of the MycroForge CLI.
After this tutorial, you should have a solid foundation to start exploring and using MycroForge to develop your
projects.
## General notes
The commands in this tutorial assume that you're running them from a MycroForge root directory.
## Initialize the project
Open a terminal and `cd` into the directory where your project should be created.
Run `m4g init todo-app` to initialize a new project and open the newly created project by running `code todo-app`.
Make sure you have `vscode` on you machine before running this command. If you prefer using another editor, then
manually open the generated `todo-app` folder.
## Setup the database
Our todo app needs to keep track of todos, so it needs a storage mechanism of some sorts. A database should be one
of the first things, if not THE first thing, that comes to mind. Luckily, MycroForge provides you with a locally hosted
database for you project out of the box. This setup, is powered by docker compose and can be examined by opening the
`db.docker-compose.yml` file in the project. Follow along to learn how to use the docker based database when developing
locally.
### Run the database
The first step is to start the database, you can do this by running the following command in a terminal.
`m4g db run`
This command starts the services defined in the `db.docker-compose.yml` file.
You can verify that the services are up by running `docker container ls`. If everything went well, then the previous
command should output the service names defined in `db.docker-compose.yml`.
If you go to [PhpMyAdmin (i.e. http://localhost:5051)](http://localhost:5051), you should now be able to login with the
following credentials.
- user: root
- pass: password
When you're done developing, you can shut down the local database by running `m4g db stop`
### Create the entities
Now that the database is running, we can start to create our entities. Run the commands below to create the `Todo` &
`Tag` entities.
`m4g db generate entity Tag --column "description:str:String(255)"`
`m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"`
After running these commands, you should find the generated entities in the `db/entities` folder of your project.
You should also see that the `main.py` & `db/env.py` files have been modified to include the newly generated entity.
For more information about the `m4g db generate entity` command, you can run `m4g db generate entity -?`.
### Define a many-to-many relation between Todo & Tag
To allow for relations between `Todo` & `Tag`, we'll define a many-to-many relation between the two entities.
This relation makes sense, because a `Todo` can have many `Tags` and a `Tag` could belong to many `Todos`.
You can generate this relation by running the following command from you terminal.
Creating a one-to-many relation would also make sense, but for the purpose of demonstration we're going to demonstrate
the many-to-many relation, because this one is the most complex, since it requires an additional mapping to be included
in the database schema.
`m4g db link many Todo --to-many Tag`
After running this command you should see that both the `Todo` and `Tag` entities now have a new field referencing the
a `List` containing instances of the other entity.
For more information about the `m4g db link` command try running `m4g db link -?`. Note that you can do the same thing
for all sub commands, so if you want to know more about `m4g db link many` you can simply run `m4g db link many -?` to
examine the command. The same is true for all the other commands as well.
### Generate the migration
Now that we've generated our entities, it's time to generate a migration that will apply these changes in the database.
Generate the initial migration by running the following command.
`m4g db generate migration initial_migration`
After running this command, you should see the new migration in the `db/version` directory.
### Apply the migration
The last step for the database setup is to actually apply the new migration to the database. This can be done by running
the following command.
`m4g db migrate`
After running this command, you should now see a populated schema when visiting [PhpMyAdmin](http://localhost:5051).
If for whatever reason you want to undo the last migration, you can simply run `m4g db rollback`.
## Setup the API
### Generate CRUD for Tag & Todo
Our API should provide use with basic endpoint to manage the `Todo` & `Tag` entities, i.e. CRUD functionality.
Writing this code can be boring, since it's pretty much boilerplate with some custom additions sprinkled here and there.
Fortunately, MycroForge can generate a good chunk of this boring code on your behalf. Run the following commands to
generate CRUD functionality for the `Todo` & `Tag` classes.
`m4g api generate crud Tag`
`m4g api generate crud Todo`
After running this command you should see that the `api/requests`,`api/routers` & `api/services` now contain the
relevant classes need to support the generated CRUD functionality. This could should be relatively straightforward, so
we won't dive into it, but feel free to take a break and explore what the generated code actually does. Another thing to
note, is that the generated routers are also automatically included in `main.py`.
### Modify the generated Todo request classes
Since we have a many-to-many relationship between `Todo` & `Tag`, the generated CRUD functionality isn't quite ready
yet. We need to be able to specify which `Tags` to add to a `Todo` when creating or updating it.
To do this, we will allow for a `tag_ids` field in both the `CreateTodoRequest` & the `UpdateTodoRequest`.
This field will contain the ids of the `Tags` that are associated with a `Todo`.
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`, you might need to import `List` from `typing`.
```python
# Before
class CreateTodoRequest(BaseModel):
description: str = None
is_done: bool = None
# After
class CreateTodoRequest(BaseModel):
description: str = None
is_done: bool = None
tag_ids: Optional[List[int]] = []
```
Modify `UpdateTodoRequest` in `api/requests/update_todo_request.py`, you might need to import `List` from `typing`.
```python
# Before
class UpdateTodoRequest(BaseModel):
description: Optional[str] = None
is_done: Optional[bool] = None
tag_ids: Optional[List[int]] = []
# After
class UpdateTodoRequest(BaseModel):
description: Optional[str] = None
is_done: Optional[bool] = None
tag_ids: Optional[List[int]] = []
```
### Modify generated TodoService
The `TodoService` will also need to be updated to accomodate the management of `tag_ids`.
Add the following imports in `api/services/todo_service.py`.
```python
from sqlalchemy.orm import selectinload
from db.entities.tag import Tag
```
Modify `TodoService.list`
```python
# Before
async with async_session() as session:
stmt = select(Todo)
results = (await session.scalars(stmt)).all()
return results
# After
async with async_session() as session:
stmt = select(Todo).options(selectinload(Todo.tags))
results = (await session.scalars(stmt)).all()
return results
```
Modify `TodoService.get_by_id`
```python
# Before
async def get_by_id(self, id: int) -> Optional[Todo]:
async with async_session() as session:
stmt = select(Todo).where(Todo.id == id)
result = (await session.scalars(stmt)).first()
return result
# After
async def get_by_id(self, id: int) -> Optional[Todo]:
async with async_session() as session:
stmt = select(Todo).where(Todo.id == id).options(selectinload(Todo.tags))
result = (await session.scalars(stmt)).first()
return result
```
Modify `TodoService.create`
```python
# Before
async def create(self, data: Dict[str, Any]) -> None:
async with async_session() as session:
entity = Todo(**data)
session.add(entity)
await session.commit()
# After
async def create(self, data: Dict[str, Any]) -> None:
tag_ids = []
if "tag_ids" in data.keys():
tag_ids = data["tag_ids"]
del data["tag_ids"]
async with async_session() as session:
entity = Todo(**data)
if len(tag_ids) > 0:
stmt = select(Tag).where(Tag.id.in_(tag_ids))
result = (await session.scalars(stmt)).all()
entity.tags = list(result)
session.add(entity)
await session.commit()
```
Modify `TodoService.update`
```python
# Before
async def update(self, id: int, data: Dict[str, Any]) -> bool:
tag_ids = []
if "tag_ids" in data.keys():
tag_ids = data["tag_ids"]
del data["tag_ids"]
async with async_session() as session:
stmt = select(Todo).where(Todo.id == id)
entity = (await session.scalars(stmt)).first()
if entity is None:
return False
else:
for key, value in data.items():
setattr(entity, key, value)
await session.commit()
return True
# After
async def update(self, id: int, data: Dict[str, Any]) -> bool:
tag_ids = []
if "tag_ids" in data.keys():
tag_ids = data["tag_ids"]
del data["tag_ids"]
async with async_session() as session:
stmt = select(Todo).where(Todo.id == id).options(selectinload(Todo.tags))
entity = (await session.scalars(stmt)).first()
if entity is None:
return False
else:
for key, value in data.items():
setattr(entity, key, value)
if len(tag_ids) > 0:
stmt = select(Tag).where(Tag.id.in_(tag_ids))
result = (await session.scalars(stmt)).all()
entity.tags = list(result)
else:
entity.tags = []
await session.commit()
return True
```
At this point, the app should be ready to test.
TODO: Elaborate!

86
docs/docusaurus.config.ts Normal file
View File

@ -0,0 +1,86 @@
import {themes as prismThemes} from 'prism-react-renderer';
import type {Config} from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
const config: Config = {
title: 'MycroForge',
tagline: 'Your FastAPI & SQLAlchemy assistant!',
favicon: 'img/favicon.ico',
// Set the production url of your site here
url: 'https://git.devdisciples.com',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'devdisciples', // Usually your GitHub org/user name.
projectName: 'mycroforge', // Usually your repo name.
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
presets: [
[
'classic',
{
docs: {
sidebarPath: './sidebars.ts',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
},
theme: {
customCss: './src/css/custom.css',
},
} satisfies Preset.Options,
],
],
themeConfig: {
// Replace with your project's social card
image: 'img/docusaurus-social-card.jpg',
navbar: {
title: 'MycroForge',
logo: {
alt: 'MycroForge Logo',
src: 'img/logo.svg',
},
items: [
{
type: 'docSidebar',
sidebarId: 'tutorialSidebar',
position: 'left',
label: 'Docs',
},
{
href: 'https://git.devdisciples.com/devdisciples/mycroforge',
label: 'Git',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
],
copyright: `Copyright © ${new Date().getFullYear()} DevDisciples`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
} satisfies Preset.ThemeConfig,
};
export default config;

14543
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
docs/package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/preset-classic": "3.4.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/tsconfig": "3.4.0",
"@docusaurus/types": "3.4.0",
"typescript": "~5.2.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}

35
docs/requirements.txt Normal file
View File

@ -0,0 +1,35 @@
asgiref==3.5.0
blinker==1.4
click==8.0.3
colorama==0.4.4
command-not-found==0.3
cryptography==3.4.8
dbus-python==1.2.18
distro==1.7.0
distro-info==1.1+ubuntu0.2
h11==0.13.0
httplib2==0.20.2
importlib-metadata==4.6.4
jeepney==0.7.1
keyring==23.5.0
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
more-itertools==8.10.0
netifaces==0.11.0
oauthlib==3.2.0
PyGObject==3.42.1
PyJWT==2.3.0
pyparsing==2.4.7
python-apt==2.4.0+ubuntu3
PyYAML==5.4.1
SecretStorage==3.3.1
six==1.16.0
systemd-python==234
ubuntu-pro-client==8001
ufw==0.36.1
unattended-upgrades==0.1
uvicorn==0.15.0
wadllib==1.3.6
wsproto==1.0.0
zipp==1.0.0

31
docs/sidebars.ts Normal file
View File

@ -0,0 +1,31 @@
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
const sidebars: SidebarsConfig = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};
export default sidebars;

View File

@ -0,0 +1,69 @@
import clsx from 'clsx';
import Heading from '@theme/Heading';
import styles from './styles.module.css';
type FeatureItem = {
title: string;
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
description: JSX.Element;
};
const FeatureList: FeatureItem[] = [
{
title: 'Initialize a skeleton project quickly',
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
description: (
<>
Initialize a skeleton project that supports FastAPI and SQLAlchemy with a single command.
Here is an example. <code>m4g init todo-app</code>
</>
),
},
{
title: 'Generate common items',
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
description: (
<>
MycroForge allows you to generate boilerplate code for common items like entities, service & routers.
It can even generate a basic CRUD setup around an entity!
</>
),
},
{
title: 'Extend MycroForge',
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
description: (
<>
Extend MycroForge with your own commands by creating a plugin!
</>
),
},
];
function Feature({title, Svg, description}: FeatureItem) {
return (
<div className={clsx('col col--4')}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<Heading as="h3">{title}</Heading>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures(): JSX.Element {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,11 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}

30
docs/src/css/custom.css Normal file
View File

@ -0,0 +1,30 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}

View File

@ -0,0 +1,23 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}

43
docs/src/pages/index.tsx Normal file
View File

@ -0,0 +1,43 @@
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import HomepageFeatures from '@site/src/components/HomepageFeatures';
import Heading from '@theme/Heading';
import styles from './index.module.css';
function HomepageHeader() {
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<Heading as="h1" className="hero__title">
{siteConfig.title}
</Heading>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/intro">
MycroForge Tutorial
</Link>
</div>
</div>
</header>
);
}
export default function Home(): JSX.Element {
const {siteConfig} = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />">
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}

0
docs/static/.nojekyll vendored Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Some files were not shown because too many files have changed in this diff Show More