aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuqian Yang <crupest@crupest.life>2025-09-04 14:34:37 +0800
committerYuqian Yang <crupest@crupest.life>2025-09-04 14:34:37 +0800
commit97b51e84b81d3afa62f316769bfea614e5b1aefc (patch)
tree2aec3195c1e4fcf3b09a2659f1526253fa5a7fa4
parent94e018f503d4a656c7f7d5b86ce3a47babfadd8d (diff)
downloadcrupest-97b51e84b81d3afa62f316769bfea614e5b1aefc.tar.gz
crupest-97b51e84b81d3afa62f316769bfea614e5b1aefc.tar.bz2
crupest-97b51e84b81d3afa62f316769bfea614e5b1aefc.zip
chore: delete works from work tree.
-rw-r--r--store/works/Crupest.SecretTool/.gitignore7
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool.sln30
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/.gitignore1
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/Config.cs95
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/Controller.cs113
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj34
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs26
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs324
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs123
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/Program.cs113
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml13
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs76
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs31
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/Routing.cs155
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs20
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs40
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs56
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs231
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs271
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs25
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs31
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/config.json.template63
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template55
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/hosts.txt2
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/proxy.txt50
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template45
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-mobile.json11
-rw-r--r--store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-pc.json14
-rwxr-xr-xstore/works/Crupest.SecretTool/build-secret.bash41
-rw-r--r--store/works/Crupest.SecretTool/build-secret.ps125
-rwxr-xr-xstore/works/Crupest.SecretTool/tools/cru-proxy-edit12
-rwxr-xr-xstore/works/Crupest.SecretTool/tools/cru-proxy-log13
-rw-r--r--store/works/Crupest.SecretTool/tools/crupest-secret-tool.service8
-rw-r--r--store/works/Crupest.SecretTool/tools/crupest-secret-tool.xml49
-rw-r--r--store/works/Crupest.SecretTool/tools/life.crupest.secret-tool.plist18
-rw-r--r--store/works/README.md9
-rw-r--r--store/works/bruno/ComfyUI/Get Object Info.bru11
-rw-r--r--store/works/bruno/ComfyUI/Get Prompt History.bru15
-rw-r--r--store/works/bruno/ComfyUI/Post Prompt.bru124
-rw-r--r--store/works/bruno/ComfyUI/Upload Image.bru18
-rw-r--r--store/works/bruno/ComfyUI/View Image.bru19
-rw-r--r--store/works/bruno/ComfyUI/bruno.json9
-rw-r--r--store/works/bruno/ComfyUI/environments/ChimerAI ComfyUI Server.bru3
-rw-r--r--store/works/python/.gitignore3
-rw-r--r--store/works/python/.python-version1
-rw-r--r--store/works/python/cru/__init__.py60
-rw-r--r--store/works/python/cru/_base.py101
-rw-r--r--store/works/python/cru/_const.py49
-rw-r--r--store/works/python/cru/_decorator.py97
-rw-r--r--store/works/python/cru/_error.py89
-rw-r--r--store/works/python/cru/_event.py61
-rw-r--r--store/works/python/cru/_func.py172
-rw-r--r--store/works/python/cru/_helper.py16
-rw-r--r--store/works/python/cru/_iter.py469
-rw-r--r--store/works/python/cru/_type.py52
-rw-r--r--store/works/python/cru/attr.py364
-rw-r--r--store/works/python/cru/config.py196
-rw-r--r--store/works/python/cru/list.py160
-rw-r--r--store/works/python/cru/parsing.py290
-rw-r--r--store/works/python/cru/service/__init__.py0
-rw-r--r--store/works/python/cru/service/__main__.py27
-rw-r--r--store/works/python/cru/service/_app.py30
-rw-r--r--store/works/python/cru/service/_base.py400
-rw-r--r--store/works/python/cru/service/_gen_cmd.py200
-rw-r--r--store/works/python/cru/service/_nginx.py263
-rw-r--r--store/works/python/cru/service/_template.py228
-rw-r--r--store/works/python/cru/system.py23
-rw-r--r--store/works/python/cru/template.py209
-rw-r--r--store/works/python/cru/tool.py82
-rw-r--r--store/works/python/cru/value.py292
-rw-r--r--store/works/python/poetry.lock111
-rw-r--r--store/works/python/pyproject.toml19
72 files changed, 0 insertions, 6493 deletions
diff --git a/store/works/Crupest.SecretTool/.gitignore b/store/works/Crupest.SecretTool/.gitignore
deleted file mode 100644
index ac4d8a4..0000000
--- a/store/works/Crupest.SecretTool/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-.vs
-bin
-obj
-*.pubxml.user
-*.csproj.user
-
-publish
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool.sln b/store/works/Crupest.SecretTool/Crupest.SecretTool.sln
deleted file mode 100644
index fde4347..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool.sln
+++ /dev/null
@@ -1,30 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.7.34024.191
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F4C2CE80-CDF8-4B08-8912-D1F0F14196AD}"
- ProjectSection(SolutionItems) = preProject
- .gitignore = .gitignore
- EndProjectSection
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crupest.SecretTool", "Crupest.SecretTool\Crupest.SecretTool.csproj", "{D6335AE4-FD22-49CD-9624-37371F3B4F82}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {B1E8FD9C-9157-4F4E-8265-4B37F30EEC5E}
- EndGlobalSection
-EndGlobal
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/.gitignore b/store/works/Crupest.SecretTool/Crupest.SecretTool/.gitignore
deleted file mode 100644
index c936492..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-vmess.txt
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Config.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Config.cs
deleted file mode 100644
index ff58551..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/Config.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-namespace Crupest.SecretTool;
-
-public record ConfigItem(string Value, int LineNumber);
-
-public class DictionaryConfig(string configString, List<string>? requiredKeys = null)
-{
- private static Dictionary<string, ConfigItem> Parse(string configString, List<string>? requiredKeys = null)
- {
- var config = new Dictionary<string, ConfigItem>();
- var lines = configString.Split('\n');
- int lineNumber = 1;
-
- foreach (var line in lines)
- {
- var l = line;
- var beginOfComment = l.IndexOf('#');
- if (beginOfComment >= 0)
- {
- l = line[..beginOfComment];
- }
- l = l.Trim();
- if (!string.IsNullOrEmpty(l))
- {
- var equalIndex = l.IndexOf('=');
- if (equalIndex == -1)
- {
- throw new FormatException($"No '=' found in line {lineNumber}.");
- }
-
- config.Add(l[..equalIndex].Trim(), new ConfigItem(l[(equalIndex + 1)..].Trim(), lineNumber));
- }
-
- lineNumber++;
- }
-
- if (requiredKeys is not null)
- {
- foreach (var key in requiredKeys)
- {
- if (!config.ContainsKey(key))
- {
- throw new FormatException($"Required key '{key}' not found in config.");
- }
- }
- }
-
- return config;
- }
-
- public string ConfigString { get; } = configString;
- public List<string>? RequiredKeys { get; } = requiredKeys;
- public Dictionary<string, ConfigItem> Config { get; } = Parse(configString);
- public ConfigItem GetItemCaseInsensitive(string key)
- {
- foreach (var (originalKey, value) in Config)
- {
- if (string.Equals(originalKey, key, StringComparison.OrdinalIgnoreCase))
- {
- return value;
- }
- }
- throw new KeyNotFoundException($"Key '{key}' not found in config case-insensitively.");
- }
-}
-
-public class ListConfig(string configString)
-{
- private static List<ConfigItem> Parse(string configString)
- {
- var config = new List<ConfigItem>();
- var lines = configString.Split('\n');
- int lineNumber = 1;
-
- foreach (var line in lines)
- {
- var l = line;
- var beginOfComment = l.IndexOf('#');
- if (beginOfComment >= 0)
- {
- l = line[..beginOfComment];
- }
- l = l.Trim();
- if (!string.IsNullOrEmpty(l))
- {
- config.Add(new ConfigItem(l, lineNumber));
- }
- lineNumber++;
- }
-
- return config;
- }
-
- public string ConfigString { get; } = configString;
- public List<ConfigItem> Config { get; } = Parse(configString);
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Controller.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Controller.cs
deleted file mode 100644
index 0803b01..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/Controller.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using System.Diagnostics;
-
-namespace Crupest.SecretTool;
-
-public class Controller(string executablePath, string configPath, string? assetPath)
-{
- public const string ToolAssetEnvironmentVariableName = "v2ray.location.asset";
-
- public static string? FindExecutable(string contentDir, out bool isLocal, string? executableName = null)
- {
- isLocal = false;
- executableName ??= "v2ray";
-
- if (OperatingSystem.IsWindows())
- {
- executableName += ".exe";
- }
-
- var localToolPath = Path.Combine(contentDir, executableName);
- if (File.Exists(localToolPath))
- {
- isLocal = true;
- return localToolPath;
- }
-
- var paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator);
- if (paths is not null)
- {
- foreach (var p in paths)
- {
- var toolPath = Path.Combine(p, executableName);
- if (File.Exists(toolPath))
- {
- return toolPath;
- }
- }
- }
-
- return null;
- }
-
- public string ExecutablePath { get; } = executablePath;
- public string ConfigPath { get; } = configPath;
- public string? AssetPath { get; } = assetPath;
- public Process? CurrentProcess { get; private set; }
-
- private Process CreateProcess()
- {
- var process = new Process();
-
- var startInfo = new ProcessStartInfo
- {
- FileName = ExecutablePath,
- };
- startInfo.ArgumentList.Add("run");
- startInfo.ArgumentList.Add("-c");
- startInfo.ArgumentList.Add(ConfigPath);
- if (AssetPath is not null)
- {
- startInfo.EnvironmentVariables[ToolAssetEnvironmentVariableName] = AssetPath;
- }
-
- process.StartInfo = startInfo;
- process.OutputDataReceived += (_, args) =>
- {
- Console.Out.Write(args.Data);
- };
- process.ErrorDataReceived += (_, args) =>
- {
- Console.Error.WriteLine(args.Data);
- };
-
- return process;
- }
-
- public void Stop()
- {
- if (CurrentProcess is not null)
- {
- CurrentProcess.Kill();
- CurrentProcess.Dispose();
- CurrentProcess = null;
- Console.WriteLine("V2ray stopped.");
- }
- }
-
- public void Start(bool stopOld = false)
- {
- if (stopOld) Stop();
-
- if (CurrentProcess is null)
- {
- CurrentProcess = CreateProcess();
- CurrentProcess.EnableRaisingEvents = true;
- CurrentProcess.Exited += (_, _) =>
- {
- if (CurrentProcess.ExitCode != 0)
- {
- const string message = "V2ray exited with error.";
- Console.Error.WriteLine(message);
- throw new Exception(message);
- }
- };
- CurrentProcess.Start();
- Console.WriteLine("V2ray started.");
- }
- }
-
- public void Restart()
- {
- Start(true);
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj b/store/works/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj
deleted file mode 100644
index 2502e74..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj
+++ /dev/null
@@ -1,34 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <OutputType>Exe</OutputType>
- <TargetFramework>net8.0</TargetFramework>
- <ImplicitUsings>enable</ImplicitUsings>
- <Nullable>enable</Nullable>
- </PropertyGroup>
-
- <ItemGroup>
- <None Update="config.json.template">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </None>
- <None Update="proxy.txt">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </None>
- <None Update="vmess.txt">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </None>
- <None Update="hosts.txt">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </None>
- <None Update="sing-config.json.template">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </None>
- <None Update="sing-inbounds-mobile.json">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </None>
- <None Update="sing-inbounds-pc.json">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </None>
- </ItemGroup>
-
-</Project>
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs
deleted file mode 100644
index 26e9231..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-namespace Crupest.SecretTool;
-
-public class FileWatcher(string directory, List<string> fileNames)
-{
- public string Directory { get; set; } = directory;
- public List<string> FileNames { get; set; } = fileNames;
-
- public delegate void OnChangedHandler();
- public event OnChangedHandler? OnChanged;
-
- public void Run()
- {
- var sourceWatcher = new FileSystemWatcher(Directory);
- foreach (var fileName in FileNames)
- {
- sourceWatcher.Filters.Add(fileName);
- }
- sourceWatcher.NotifyFilter = NotifyFilters.LastWrite;
-
- while (true)
- {
- var result = sourceWatcher.WaitForChanged(WatcherChangeTypes.Changed | WatcherChangeTypes.Created);
- OnChanged?.Invoke();
- }
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs
deleted file mode 100644
index 8f4c171..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs
+++ /dev/null
@@ -1,324 +0,0 @@
-using System.IO.Compression;
-
-namespace Crupest.SecretTool;
-
-public interface IGeoSiteEntry
-{
- bool IsInclude { get; }
- string Value { get; }
-}
-
-public record GeoSiteIncludeEntry(string Value, string ContainingSite) : IGeoSiteEntry
-{
- public bool IsInclude => true;
-}
-
-public record GeoSiteRuleEntry(HostMatchKind Kind, string Value, List<string> Attributes, string ContainingSite) : IGeoSiteEntry
-{
- public bool IsInclude => false;
-
- public RoutingRuleMatcher GetRoutingRuleMatcher() => new(Kind, Value);
-}
-
-public record GeoSite(string Name, List<IGeoSiteEntry> Entries)
-{
- public static GeoSite Parse(string name, string str)
- {
- List<IGeoSiteEntry> entries = [];
- var listConfig = new ListConfig(str);
- foreach (var item in listConfig.Config)
- {
- var (value, line) = item;
-
- if (value.StartsWith("include:"))
- {
- var include = value["include:".Length..].Trim();
- if (include.Length == 0 || include.Contains(' '))
- {
- throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid include value.");
- }
- entries.Add(new GeoSiteIncludeEntry(include, name));
- continue;
- }
-
- var segments = value.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
- if (segments.Length > 2)
- {
- throw new FormatException($"Invalid geo site rule '{name}' in line {line}. More than one ':'.");
- }
-
- HostMatchKind kind;
- if (segments.Length == 2)
- {
- kind = segments[0] switch
- {
- "domain" => kind = HostMatchKind.DomainSuffix,
- "full" => kind = HostMatchKind.DomainFull,
- "keyword" => kind = HostMatchKind.DomainKeyword,
- "regexp" => kind = HostMatchKind.DomainRegex,
- _ => throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Unknown matcher.")
- };
- }
- else
- {
- kind = HostMatchKind.DomainSuffix;
- }
-
- var domainSegments = segments[^1].Split('@', StringSplitOptions.TrimEntries);
- var domain = domainSegments[0];
- if (kind != HostMatchKind.DomainRegex && Uri.CheckHostName(domain) != UriHostNameType.Dns)
- {
- throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid domain.");
- }
-
- List<string> attributes = [];
- foreach (var s in domainSegments[1..])
- {
- if (s.Length == 0)
- {
- throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Empty attribute value.");
- }
- attributes.Add(s);
- }
-
- entries.Add(new GeoSiteRuleEntry(kind, domain, attributes, name));
- }
- return new GeoSite(name, entries);
- }
-}
-
-public class GeoSiteData(string directory)
-{
- private static List<GeoSite> Parse(string directory)
- {
- var sites = new List<GeoSite>();
- foreach (var path in Directory.GetFileSystemEntries(directory))
- {
- var content = File.ReadAllText(path);
- sites.Add(GeoSite.Parse(Path.GetFileName(path), content));
- }
- return sites;
- }
-
- public string DataDirectory { get; } = directory;
-
- public List<GeoSite> Sites { get; } = Parse(directory);
-
- public GeoSite? GetSite(string name)
- {
- return Sites.Where(s => s.Name == name).FirstOrDefault();
- }
-
- public List<GeoSiteRuleEntry> GetEntriesRecursive(List<string> sites,
- List<HostMatchKind>? onlyMatcherKinds = null, List<string>? onlyAttributes = null)
- {
- List<GeoSiteRuleEntry> entries = [];
- HashSet<string> visited = [];
- HashSet<HostMatchKind>? kinds = onlyMatcherKinds?.ToHashSet();
-
- void Visit(string site)
- {
- if (visited.Contains(site))
- {
- return;
- }
-
- visited.Add(site);
- var siteData = GetSite(site);
- if (siteData == null)
- {
- return;
- }
- foreach (var entry in siteData.Entries)
- {
- if (entry is GeoSiteIncludeEntry includeEntry)
- {
- Visit(includeEntry.Value);
- }
- else if (entry is GeoSiteRuleEntry geoSiteRuleEntry)
- {
- if (kinds != null && !kinds.Contains(geoSiteRuleEntry.Kind))
- {
- continue;
- }
-
- if (onlyAttributes != null && !geoSiteRuleEntry.Attributes.Intersect(onlyAttributes).Any())
- {
- continue;
- }
-
- entries.Add(geoSiteRuleEntry);
- }
- }
- }
-
- foreach (var s in sites)
- {
- Visit(s);
- }
-
- return entries;
- }
-}
-
-public class GeoDataManager
-{
- public const string GeoSiteFileName = "geosite.dat";
- public const string GeoIpFileName = "geoip.dat";
- public const string GeoIpCnFileName = "geoip-only-cn-private.dat";
-
- public static class ToolGithub
- {
- public const string Organization = "v2fly";
- public const string GeoSiteRepository = "domain-list-community";
- public const string GeoIpRepository = "geoip";
- public const string GeoSiteReleaseFilename = "dlc.dat";
- public const string GeoIpReleaseFilename = "geoip.dat";
- public const string GeoIpCnReleaseFilename = "geoip-only-cn-private.dat";
- }
-
- public static GeoDataManager Instance { get; } = new GeoDataManager();
-
- public record GeoDataAsset(string Name, string FileName, string GithubUser, string GithubRepo, string GithubReleaseFileName);
-
- public GeoDataManager()
- {
- Assets =
- [
- new("geosite", GeoSiteFileName, ToolGithub.Organization, ToolGithub.GeoSiteRepository, ToolGithub.GeoSiteReleaseFilename),
- new("geoip", GeoIpFileName, ToolGithub.Organization, ToolGithub.GeoIpRepository, ToolGithub.GeoIpReleaseFilename),
- new("geoip-cn", GeoIpCnFileName, ToolGithub.Organization, ToolGithub.GeoIpRepository, ToolGithub.GeoIpCnReleaseFilename),
- ];
- }
-
- public List<GeoDataAsset> Assets { get; set; }
-
- public GeoSiteData? GeoSiteData { get; set; }
-
- public GeoSiteData GetOrCreateGeoSiteData(bool clean, bool silent)
- {
- if (GeoSiteData is not null) { return GeoSiteData; }
- GeoSiteData = DownloadAndGenerateGeoSiteData(clean, silent);
- return GeoSiteData;
- }
-
- private static string GetReleaseFileUrl(string user, string repo, string fileName)
- {
- return $"https://github.com/{user}/{repo}/releases/latest/download/{fileName}";
- }
-
- private static void GithubDownloadRelease(HttpClient httpClient, string user, string repo, string fileName, string outputPath, bool silent)
- {
- var url = GetReleaseFileUrl(user, repo, fileName);
- if (!silent) Console.WriteLine($"Downloading {url} to {outputPath}");
- using var responseStream = httpClient.GetStreamAsync(url).Result;
- using var outputFileStream = File.OpenWrite(outputPath);
- responseStream.CopyTo(outputFileStream);
- }
-
- public bool HasAllAssets(string directory, out List<string> missing)
- {
- missing = [];
- foreach (var asset in Assets)
- {
- var assetPath = Path.Combine(directory, asset.FileName);
- if (!File.Exists(assetPath))
- {
- missing.Add(asset.Name);
- }
- }
- return missing.Count == 0;
- }
-
- public void Download(string outputDir, bool silent)
- {
- using var httpClient = new HttpClient();
-
- foreach (var asset in Assets)
- {
- if (!silent)
- {
- Console.WriteLine($"Downloading {asset.Name}...");
- }
- GithubDownloadRelease(httpClient, asset.GithubUser, asset.GithubRepo, asset.GithubReleaseFileName, Path.Combine(outputDir, asset.FileName), silent);
- if (!silent)
- {
- Console.WriteLine($"Downloaded {asset.Name}!");
- }
- }
-
- if (!File.Exists(Program.RestartLabelFilePath))
- {
- File.Create(Program.RestartLabelFilePath);
- }
- else
- {
- File.SetLastWriteTime(Program.RestartLabelFilePath, DateTime.Now);
- }
- }
-
- private static string GetGithubRepositoryArchiveUrl(string user, string repo)
- {
- return $"https://github.com/{user}/{repo}/archive/refs/heads/master.zip";
- }
-
- private static void GithubDownloadRepository(HttpClient httpClient, string user, string repo, string outputPath, bool silent)
- {
- var url = GetGithubRepositoryArchiveUrl(user, repo);
- if (!silent) { Console.WriteLine($"Begin to download data from {url} to {outputPath}."); }
- using var responseStream = httpClient.GetStreamAsync(url).Result;
- using var outputFileStream = File.OpenWrite(outputPath);
- responseStream.CopyTo(outputFileStream);
- if (!silent) { Console.WriteLine("Succeeded to download."); }
- }
-
- private static void Unzip(string zipPath, string outputPath)
- {
- using var zip = ZipFile.OpenRead(zipPath) ?? throw new Exception($"Failed to open zip file {zipPath}");
- zip.ExtractToDirectory(outputPath);
- }
-
- private static string DownloadAndExtractGeoDataRepository(bool cleanTempDirIfFailed, bool silent, out string tempDirectoryPath)
- {
- tempDirectoryPath = "";
- const string zipFileName = "v2ray-geosite-master.zip";
- using var httpClient = new HttpClient();
- var tempDirectory = Directory.CreateTempSubdirectory(Program.Name);
- tempDirectoryPath = tempDirectory.FullName;
- try
- {
- var archivePath = Path.Combine(tempDirectoryPath, zipFileName);
- var extractPath = Path.Combine(tempDirectoryPath, "repo");
- GithubDownloadRepository(httpClient, ToolGithub.Organization, ToolGithub.GeoSiteRepository, archivePath, silent);
- if (!silent) { Console.WriteLine($"Extract geo data to {extractPath}."); }
- Directory.CreateDirectory(extractPath);
- Unzip(archivePath, extractPath);
- if (!silent) { Console.WriteLine($"Extraction done."); }
- return Path.Join(extractPath, "domain-list-community-master");
- }
- catch (Exception)
- {
- if (cleanTempDirIfFailed)
- {
- Directory.Delete(tempDirectoryPath, true);
- }
- throw;
- }
- }
-
- private static GeoSiteData DownloadAndGenerateGeoSiteData(bool clean, bool silent)
- {
- var repoDirectory = DownloadAndExtractGeoDataRepository(clean, silent, out var tempDirectoryPath);
- try
- {
- return new GeoSiteData(Path.Join(repoDirectory, "data"));
- }
- finally
- {
- if (clean)
- {
- Directory.Delete(tempDirectoryPath, true);
- }
- }
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs
deleted file mode 100644
index 858333d..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs
+++ /dev/null
@@ -1,123 +0,0 @@
-namespace Crupest.SecretTool;
-
-public enum HostMatchKind
-{
- DomainFull,
- DomainSuffix,
- DomainKeyword,
- DomainRegex,
- Ip,
- GeoSite,
- GeoIp,
-}
-
-public static class HostMatchKindExtensions
-{
- public static bool IsDomain(this HostMatchKind kind)
- {
- return kind.IsNonRegexDomain() || kind == HostMatchKind.DomainRegex;
- }
-
- public static bool IsNonRegexDomain(this HostMatchKind kind)
- {
- return kind is HostMatchKind.DomainFull or HostMatchKind.DomainSuffix or HostMatchKind.DomainKeyword;
- }
-
-
- public static List<HostMatchKind> DomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword, HostMatchKind.DomainRegex];
-
- public static List<HostMatchKind> NonRegexDomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword];
-
- public static List<HostMatchKind> SupportedInSingRouteMatchKinds { get; } = [..DomainMatchKinds, HostMatchKind.Ip];
-
- public static bool IsSupportedInSingRoute(this HostMatchKind kind) => SupportedInSingRouteMatchKinds.Contains(kind);
-}
-
-public record HostMatchConfigItem(HostMatchKind Kind, string MatchString, List<string> Values);
-
-public class HostMatchConfig(string configString, List<HostMatchKind> allowedMatchKinds, int minComponentCount = -1, int maxComponentCount = -1)
-{
- private static List<HostMatchConfigItem> Parse(string configString, List<HostMatchKind> allowedMatchKinds, int minComponentCount = -1, int maxComponentCount = -1)
- {
- var items = new ListConfig(configString).Config;
- var result = new List<HostMatchConfigItem>();
-
- foreach (var item in items)
- {
- var lineNumber = item.LineNumber;
- var line = item.Value;
- var hasExplicitMatchKind = false;
- var segments = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList();
-
- foreach (var matchKind in Enum.GetValues<HostMatchKind>())
- {
- var matchKindName = Enum.GetName(matchKind) ?? throw new Exception("No such match kind.");
- if (segments[0] == matchKindName)
- {
- hasExplicitMatchKind = true;
-
- if (segments.Count < 2)
- {
- throw new FormatException($"Explicit match item needs a value in line {lineNumber}.");
- }
- if (allowedMatchKinds.Contains(matchKind))
- {
- if (matchKind.IsNonRegexDomain() && Uri.CheckHostName(matchKindName) != UriHostNameType.Dns)
- {
- throw new FormatException($"Invalid domain format in line {lineNumber}.");
- }
-
- var components = segments[2..].ToList();
- if (minComponentCount > 0 && components.Count < minComponentCount)
- {
- throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required.");
- }
- if (maxComponentCount >= 0 && components.Count > maxComponentCount)
- {
- throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed.");
- }
- result.Add(new HostMatchConfigItem(matchKind, segments[1], components));
- }
- else
- {
- throw new FormatException($"Match kind {matchKindName} is not allowed at line {lineNumber}.");
- }
- }
- }
-
- if (!hasExplicitMatchKind)
- {
- if (minComponentCount > 0 && segments.Count - 1 < minComponentCount)
- {
- throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required.");
- }
- if (maxComponentCount >= 0 && segments.Count - 1 > maxComponentCount)
- {
- throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed.");
- }
- result.Add(new HostMatchConfigItem(HostMatchKind.DomainSuffix, segments[0], segments.Count == 1 ? [] : segments[1..]));
- }
- }
- return result;
- }
-
- public string ConfigString { get; } = configString;
- public List<HostMatchKind> AllowedMatchKinds { get; } = allowedMatchKinds;
- public int MinComponentCount { get; } = minComponentCount;
- public int MaxComponentCount { get; } = maxComponentCount;
- public List<HostMatchConfigItem> Items { get; } = Parse(configString, allowedMatchKinds, minComponentCount, maxComponentCount);
-}
-
-public class HostMatchConfigFile
-{
- public HostMatchConfigFile(string path, List<HostMatchKind> allowedMatchKinds, int minComponentCount = -1, int maxComponentCount = -1)
- {
- Path = path;
- FileContent = File.ReadAllText(path);
- Config = new HostMatchConfig(FileContent, allowedMatchKinds, minComponentCount, maxComponentCount); ;
- }
-
- public string Path { get; }
- public string FileContent { get; }
- public HostMatchConfig Config { get; }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Program.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Program.cs
deleted file mode 100644
index 18b1ac0..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/Program.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using System.Reflection;
-
-namespace Crupest.SecretTool;
-
-public static class Program
-{
- public static string Name { get; } = typeof(Program).Namespace ?? throw new Exception("Can't get the name of Crupest.SecretTool.");
-
- public static string CrupestSecretToolDirectory { get; } =
- Environment.GetEnvironmentVariable("CRUPEST_V2RAY_DIR") ??
- Path.GetFullPath(Path.GetDirectoryName(
- Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Can't get the path of Crupest.SecretTool."));
-
- private const string ConfigOutputFileName = "config.json";
- private const string SurgeRuleSetChinaOutputFileName = "ChinaRuleSet.txt";
- private const string SurgeRuleSetGlobalOutputFileName = "GlobalRuleSet.txt";
-
- public const string RestartLabelFileName = "restart.label";
- public static string RestartLabelFilePath { get; } = Path.Combine(CrupestSecretToolDirectory, RestartLabelFileName);
-
- public static void RunToolAndWatchConfigChange()
- {
- var executablePath = Controller.FindExecutable(CrupestSecretToolDirectory, out var isLocal) ??
- throw new Exception("Can't find v2ray executable either in Crupest.SecretTool directory or in PATH.");
-
- string? assetsPath;
- if (isLocal)
- {
- assetsPath = CrupestSecretToolDirectory;
- var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestSecretToolDirectory, out var missing);
- if (!assetsComplete)
- {
- throw new Exception($"Missing assets: {string.Join(", ", missing)} in {CrupestSecretToolDirectory}. This v2ray is local. So only use assets in Crupest.SecretTool directory.");
- }
- }
- else
- {
- assetsPath = CrupestSecretToolDirectory;
- var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestSecretToolDirectory, out var missing);
- if (!assetsComplete)
- {
- Console.WriteLine($"Missing assets: {string.Join(", ", missing)} in {CrupestSecretToolDirectory}. This v2ray is global. So fallback to its own assets.");
- assetsPath = null;
- }
- }
-
- var controller = new Controller(executablePath, Path.Combine(CrupestSecretToolDirectory, ConfigOutputFileName), assetsPath);
- var configFileWatcher = new FileWatcher(CrupestSecretToolDirectory,
- [.. ToolConfig.ConfigFileNames, RestartLabelFileName]);
-
- ToolConfig.FromDirectoryAndWriteToFile(CrupestSecretToolDirectory, Path.Join(CrupestSecretToolDirectory, ConfigOutputFileName));
- controller.Start();
-
- configFileWatcher.OnChanged += () =>
- {
- ToolConfig.FromDirectoryAndWriteToFile(CrupestSecretToolDirectory, Path.Join(CrupestSecretToolDirectory, ConfigOutputFileName));
- controller.Restart();
- };
-
- configFileWatcher.Run();
- }
-
- public static void Main(string[] args)
- {
- if (args.Length != 0)
- {
- var verb = args[0].ToLower();
- if (verb == "download-geodata" || verb == "dg")
- {
- if (args.Length != 1)
- {
- throw new Exception("Invalid command line arguments. download-geodata requires no arguments.");
- }
- GeoDataManager.Instance.Download(CrupestSecretToolDirectory, false);
- return;
- }
- else if (verb == "generate-surge-rule-set" || verb == "gsr")
- {
- if (args.Length != 1)
- {
- throw new Exception("Invalid command line arguments. download-geodata requires no arguments.");
- }
- SurgeConfigGenerator.GenerateTo(
- CrupestSecretToolDirectory,
- Path.Join(CrupestSecretToolDirectory, SurgeRuleSetChinaOutputFileName),
- Path.Join(CrupestSecretToolDirectory, SurgeRuleSetGlobalOutputFileName),
- true, true
- );
- return;
- }
- else if (verb == "generate-sing-config" || verb == "gs")
- {
- if (args.Length != 2 || args[1].ToLower() is not ("pc" or "mobile"))
- {
- throw new Exception("Invalid command line arguments. generate-sing-config requires 1 argument. The argument must be either 'pc' or 'mobile'.");
- }
-
- var config = SingToolConfig.FromDirectory(CrupestSecretToolDirectory, args[1].ToLower() == "mobile", true, true);
- Console.Out.WriteLine(config.ToSingConfigString());
- return;
- }
- else if (verb == "generate" || verb == "g")
- {
- var config = ToolConfig.FromDirectory(CrupestSecretToolDirectory);
- Console.Out.WriteLine(config.ToJsonStringV4());
- return;
- }
- throw new Exception("Invalid command line arguments.");
- }
-
- RunToolAndWatchConfigChange();
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml b/store/works/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml
deleted file mode 100644
index 5fca454..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-https://go.microsoft.com/fwlink/?LinkID=208121.
--->
-<Project>
- <PropertyGroup>
- <Configuration>Release</Configuration>
- <Platform>Any CPU</Platform>
- <PublishDir>bin\Release\net8.0\publish\</PublishDir>
- <PublishProtocol>FileSystem</PublishProtocol>
- <_TargetId>Folder</_TargetId>
- </PropertyGroup>
-</Project> \ No newline at end of file
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs
deleted file mode 100644
index d2703ba..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-namespace Crupest.SecretTool;
-
-public abstract class Proxy(string tag) : IV4ConfigObject, ISingConfigObject
-{
- public string Tag { get; set; } = tag;
-
- public abstract V4ConfigJsonObjects.Outbound ToJsonObjectV4();
- public abstract SingConfigJsonObjects.OutboundBase ToJsonObjectSing();
-
- object IV4ConfigObject.ToJsonObjectV4()
- {
- return ToJsonObjectV4();
- }
-
- object ISingConfigObject.ToJsonObjectSing()
- {
- return ToJsonObjectSing();
- }
-}
-
-public class HttpProxy(string host, int port, string tag) : Proxy(tag)
-{
- public string Host { get; set; } = host;
- public int Port { get; set; } = port;
-
- public override SingConfigJsonObjects.OutboundBase ToJsonObjectSing()
- {
- throw new NotImplementedException("Http proxy is not supported in sing now.");
- }
-
- public override V4ConfigJsonObjects.Outbound ToJsonObjectV4()
- {
- return new V4ConfigJsonObjects.Outbound(Tag, "http",
- new V4ConfigJsonObjects.HttpOutboundSettings([new V4ConfigJsonObjects.HttpOutboundServer(Host, Port, [])]),
- null
- );
- }
-}
-
-
-public class VmessProxy(string host, int port, string userId, string path, string tag) : Proxy(tag)
-{
- public string Host { get; set; } = host;
- public int Port { get; set; } = port;
- public string Path { get; set; } = path;
- public string UserId { get; set; } = userId;
-
- public override SingConfigJsonObjects.OutboundBase ToJsonObjectSing()
- {
- return new SingConfigJsonObjects.VmessOutbound(Tag, Host, Port, UserId,
- Transport: new SingConfigJsonObjects.V2rayWebsocketTransport(Path, new Dictionary<string, string> { { "Host", Host } }),
- Tls: new SingConfigJsonObjects.OutboundTls(true));
- }
-
- public override V4ConfigJsonObjects.Outbound ToJsonObjectV4()
- {
- return new V4ConfigJsonObjects.Outbound(Tag, "vmess",
- new V4ConfigJsonObjects.VmessOutboundSettings(
- [new V4ConfigJsonObjects.VnextServer(Host, Port, [new V4ConfigJsonObjects.VnextServerUser(UserId, 0, "auto", 0)])]),
- new V4ConfigJsonObjects.WsStreamSettings("ws", "tls", new V4ConfigJsonObjects.WsSettings(Path, new() { ["Host"] = Host }))
- );
- }
-
- public static VmessProxy CreateFromConfigString(string configString, string tag)
- {
- var config = new DictionaryConfig(configString, ["host", "port", "userid", "path"]);
- var portString = config.GetItemCaseInsensitive("port").Value;
- if (!int.TryParse(portString, out var port) || port <= 0)
- {
- throw new FormatException($"Invalid port number: {portString}: not an integer or is a invalid number.");
- }
- return new VmessProxy(config.GetItemCaseInsensitive("host").Value, port,
- config.GetItemCaseInsensitive("userid").Value, config.GetItemCaseInsensitive("path").Value, tag
- );
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs
deleted file mode 100644
index 81698a3..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace Crupest.SecretTool;
-
-public class ProxyFile : HostMatchConfigFile
-{
- public ProxyFile(string path) : base(path, [.. Enum.GetValues<HostMatchKind>()], maxComponentCount: 0)
- {
- RoutingRuleMatchers = Config.Items.Select(i => new RoutingRuleMatcher(i.Kind, i.MatchString)).ToList();
- }
-
- public List<RoutingRuleMatcher> RoutingRuleMatchers { get; }
-
- public List<RoutingRuleMatcher> GetChinaRulesByGeoSite(GeoSiteData geoSiteData)
- {
- var geoSites = RoutingRuleMatchers.Where(m => m.MatchKind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList();
- return geoSiteData.GetEntriesRecursive(geoSites, HostMatchKindExtensions.DomainMatchKinds, ["cn"]).Select(e => e.GetRoutingRuleMatcher()).ToList();
- }
-
- public List<RoutingRuleMatcher> GetRulesFlattenGeoSite(GeoSiteData geoSiteData, bool noCn = false)
- {
- var geoSites = RoutingRuleMatchers.Where(m => m.MatchKind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList();
- var flattenGeoSiteRules = geoSiteData.GetEntriesRecursive(geoSites, HostMatchKindExtensions.DomainMatchKinds)
- .Where(e => !noCn || !e.Attributes.Contains("cn"))
- .Select(e => e.GetRoutingRuleMatcher())
- .ToList();
- var otherRules = RoutingRuleMatchers.Where(m => m.MatchKind != HostMatchKind.GeoSite).ToList();
- return [
- ..flattenGeoSiteRules,
- ..otherRules
- ];
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Routing.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Routing.cs
deleted file mode 100644
index fdf1b93..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/Routing.cs
+++ /dev/null
@@ -1,155 +0,0 @@
-namespace Crupest.SecretTool;
-
-public record RoutingRuleMatcher(HostMatchKind MatchKind, string MatchString)
-{
- public RoutingRule ToRoutingRule(string OutboundTag) => new(MatchKind, MatchString, OutboundTag);
-}
-
-public record RoutingRule(HostMatchKind MatchKind, string MatchString, string OutboundTag) : IV4ConfigObject
-{
- public string ToolConfigString => MatchKind switch
- {
- HostMatchKind.DomainFull => $"full:{MatchString}",
- HostMatchKind.DomainSuffix => $"domain:{MatchString}",
- HostMatchKind.DomainKeyword => MatchString,
- HostMatchKind.DomainRegex => $"regexp:{MatchString}",
- HostMatchKind.Ip => MatchString,
- HostMatchKind.GeoSite => $"geosite:{MatchString}",
- HostMatchKind.GeoIp => $"geoip:{MatchString}",
- _ => throw new ArgumentException("Invalid matcher kind.")
- };
-
- public string ToolConfigStringSing => MatchKind.IsSupportedInSingRoute() ? MatchString : throw new ArgumentException("Unsupported matcher kind for sing.");
-
- public static Dictionary<string, List<RoutingRule>> GroupByOutboundTag(List<RoutingRule> rules)
- => rules.GroupBy(r => r.OutboundTag).Select(g => (g.Key, g.ToList())).ToDictionary();
-
- public static Dictionary<HostMatchKind, List<RoutingRule>> GroupByMatchKind(List<RoutingRule> rules)
- => rules.GroupBy(r => r.MatchKind).Select(g => (g.Key, g.ToList())).ToDictionary();
-
- public static List<List<RoutingRule>> GroupByOutboundTagAndMatcherKind(List<RoutingRule> rules)
- => GroupByOutboundTag(rules).Values.SelectMany((groupByTag) => GroupByMatchKind(groupByTag).Values).ToList();
-
- public static SingConfigJsonObjects.RouteRule ListToJsonObjectSing(List<RoutingRule> rules)
- {
- if (rules.Count == 0)
- {
- throw new ArgumentException("Rule list is empty.");
- }
-
- var outboundTag = rules[0].OutboundTag;
-
- if (rules.Any(r => !r.MatchKind.IsSupportedInSingRoute()))
- {
- throw new ArgumentException("Rules must have matcher kinds supported in sing.");
- }
-
- if (rules.Any(r => r.OutboundTag != outboundTag))
- {
- throw new ArgumentException("Rules must have the same outbound tag.");
- }
-
- return new SingConfigJsonObjects.RouteRule(Outbound: outboundTag,
- Domain: rules.Where(r => r.MatchKind == HostMatchKind.DomainFull).Select(r => r.ToolConfigStringSing).ToList(),
- DomainSuffix: rules.Where(r => r.MatchKind == HostMatchKind.DomainSuffix).Select(r => r.ToolConfigStringSing).ToList(),
- DomainKeyword: rules.Where(r => r.MatchKind == HostMatchKind.DomainKeyword).Select(r => r.ToolConfigStringSing).ToList(),
- DomainRegex: rules.Where(r => r.MatchKind == HostMatchKind.DomainRegex).Select(r => r.ToolConfigStringSing).ToList(),
- IpCidr: rules.Where(r => r.MatchKind == HostMatchKind.Ip).Select(r => r.ToolConfigStringSing).ToList()
- );
- }
-
- public static V4ConfigJsonObjects.RoutingRule ListToJsonObject(List<RoutingRule> rules)
- {
- if (rules.Count == 0)
- {
- throw new ArgumentException("Rule list is empty.");
- }
-
- var matchKind = rules[0].MatchKind;
- var outboundTag = rules[0].OutboundTag;
-
- if (rules.Any(r => r.OutboundTag != outboundTag) || rules.Any(r => r.MatchKind != matchKind))
- {
- throw new ArgumentException("Rules must have the same matcher kind and outbound tag.");
- }
-
- List<string> toolConfigList = rules.Select(r => r.ToolConfigString).ToList();
-
- return new V4ConfigJsonObjects.RoutingRule(OutboundTag: outboundTag,
- Ip: (matchKind is HostMatchKind.Ip or HostMatchKind.GeoIp) ? toolConfigList : null,
- Domains: (matchKind.IsDomain() || matchKind == HostMatchKind.GeoSite) ? toolConfigList : null
- );
- }
-
- public RoutingRule CloneGeositeWithCnAttribute(string outboundTag)
- {
- if (MatchKind is not HostMatchKind.GeoSite)
- {
- throw new ArgumentException("Matcher kind must be GeoSite.");
- }
-
- return new RoutingRule(HostMatchKind.GeoSite, $"{MatchString}@cn", outboundTag);
- }
-
- public RoutingRuleMatcher GetMatcher() => new(MatchKind, MatchString);
-
- public V4ConfigJsonObjects.RoutingRule ToJsonObjectV4() => ListToJsonObject([this]);
-
- object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4();
-}
-
-public record Routing(List<RoutingRule> Rules) : IV4ConfigObject, ISingConfigObject
-{
- public List<RoutingRule> CreateGeositeCnDirectRules()
- {
- return Rules.Where(r => r.MatchKind is HostMatchKind.GeoSite)
- .Select(r => r.CloneGeositeWithCnAttribute("direct")).ToList();
- }
-
- public SingConfigJsonObjects.Route ToJsonObjectSing()
- {
- List<SingConfigJsonObjects.RouteRule> ruleJsonObjects = [ new SingConfigJsonObjects.RouteRule(Outbound: "dns-out", Protocol: "dns")];
- ruleJsonObjects.AddRange(RoutingRule.GroupByOutboundTag(Rules).Values.Select(RoutingRule.ListToJsonObjectSing));
- return new SingConfigJsonObjects.Route(ruleJsonObjects);
- }
-
- public V4ConfigJsonObjects.Routing ToJsonObjectV4(string domainStrategy = "IpOnDemand", bool directGeositeCn = true)
- {
- List<V4ConfigJsonObjects.RoutingRule> ruleJsonObjects = [];
-
- if (directGeositeCn)
- {
- ruleJsonObjects.Add(RoutingRule.ListToJsonObject(CreateGeositeCnDirectRules()));
- }
-
- ruleJsonObjects.AddRange(RoutingRule.GroupByOutboundTagAndMatcherKind(Rules).Select(RoutingRule.ListToJsonObject));
-
- return new V4ConfigJsonObjects.Routing(ruleJsonObjects, domainStrategy);
- }
-
- object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4();
-
- object ISingConfigObject.ToJsonObjectSing() => ToJsonObjectSing();
-
- public static Routing FromProxyFile(ProxyFile proxyFile, string outboundTag)
- {
- return new Routing(
- proxyFile.RoutingRuleMatchers.Select(m => m.ToRoutingRule(outboundTag)).ToList());
- }
-
- public static Routing FromProxyFileForSing(ProxyFile proxyFile, GeoSiteData geoSiteData, string outboundTag, string? directCnOutboundTag = null)
- {
- List<RoutingRule> rules = [];
-
- if (directCnOutboundTag is not null)
- {
- rules.AddRange(proxyFile.GetChinaRulesByGeoSite(geoSiteData).Select(m => m.ToRoutingRule(directCnOutboundTag)).ToList());
- }
-
- rules.AddRange(proxyFile.GetRulesFlattenGeoSite(geoSiteData).Where(m => m.MatchKind.IsSupportedInSingRoute()).Select(m => m.ToRoutingRule(outboundTag)).ToList());
-
- return new Routing(
- rules
- );
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs
deleted file mode 100644
index 56b5563..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace Crupest.SecretTool;
-
-public static class SingConfigJsonObjects
-{
- public interface IObject;
-
- public record OutboundTls(bool Enabled);
- public record V2rayTransportBase(string Type);
- public record V2rayWebsocketTransport(string Path, Dictionary<string, string>? Headers = null) : V2rayTransportBase("ws");
- public record OutboundBase(string Tag, string Type) : IObject;
- public record VmessOutbound(string Tag, string Server, int ServerPort, string Uuid, string Security = "auto",
- V2rayTransportBase? Transport = null, OutboundTls? Tls = null): OutboundBase(Tag, "vmess");
-
- public record RouteRule(List<string>? Domain = null, List<string>? DomainSuffix = null, List<string>? DomainKeyword = null,
- List<string>? DomainRegex = null, List<string>? IpCidr = null, List<string>? SourceIpCidr = null, string? Protocol = null,
- List<int>? Port = null, List<int>? SourcePort = null, List<string>? PortRange = null, List<string>? SourcePortRange = null,
- string? Network = null, List<string>? Inbound = null, string? Outbound = null) : IObject;
-
- public record Route(List<RouteRule> Rules) : IObject;
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs
deleted file mode 100644
index b112e1c..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-namespace Crupest.SecretTool;
-
-public record StaticHostRule(HostMatchKind MatchKind, string MatchString, List<string> ResolveResult)
-{
- public string AddressString()
- {
- return MatchKind switch
- {
- HostMatchKind.DomainFull => MatchString,
- HostMatchKind.DomainSuffix => $"domain:{MatchString}",
- HostMatchKind.DomainKeyword => $"keyword:{MatchString}",
- HostMatchKind.DomainRegex => $"regexp:{MatchString}",
- _ => throw new ArgumentOutOfRangeException($"Match kind {MatchKind} is not allowed in static host rule."),
- };
- }
-
- public object ResolveResultToJsonObject()
- {
- return ResolveResult.Count == 1 ? ResolveResult[0] : ResolveResult;
- }
-}
-
-public class StaticHosts(List<StaticHostRule> rules) : IV4ConfigObject
-{
- public List<StaticHostRule> Rules { get; } = rules;
-
- public Dictionary<string, object> ToJsonObjectV4() =>
- Rules.ToDictionary(rule => rule.AddressString(), rule => rule.ResolveResultToJsonObject());
-
- object IV4ConfigObject.ToJsonObjectV4()
- {
- return ToJsonObjectV4();
- }
-
- public static StaticHosts CreateFromHostMatchConfigString(string configString)
- {
- var config = new HostMatchConfig(configString, HostMatchKindExtensions.DomainMatchKinds, minComponentCount: 1);
- return new StaticHosts(config.Items.Select(i => new StaticHostRule(i.Kind, i.MatchString, [.. i.Values])).ToList());
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs
deleted file mode 100644
index 8a57c9f..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-namespace Crupest.SecretTool;
-
-public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoData)
-{
- public ProxyFile ProxyFile => proxyFile;
- public GeoSiteData GeoData => geoData;
-
- private static string ToSurgeRuleString(HostMatchKind kind, string value)
- {
- var ruleType = kind switch
- {
- HostMatchKind.DomainFull => "DOMAIN",
- HostMatchKind.DomainSuffix => "DOMAIN-SUFFIX",
- HostMatchKind.DomainKeyword => "DOMAIN-KEYWORD",
- HostMatchKind.DomainRegex => "URL-REGEX",
- _ => throw new Exception("Unacceptable matcher kind for Surge rule.")
- };
-
- return $"{ruleType},{value}";
- }
-
- public static string GenerateSurgeRuleSetString(List<RoutingRuleMatcher> rules)
- {
- return string.Join('\n', rules.Select(r => ToSurgeRuleString(r.MatchKind, r.MatchString)));
- }
-
- public string GenerateChinaRuleSet()
- {
- return GenerateSurgeRuleSetString(proxyFile.GetChinaRulesByGeoSite(GeoData));
- }
-
- public string GenerateGlobalRuleSet()
- {
- return GenerateSurgeRuleSetString(proxyFile.GetRulesFlattenGeoSite(geoData, true));
- }
-
- public static void GenerateTo(ProxyFile proxyFile, GeoSiteData geoSiteData, string cnPath, string globalPath, bool silent)
- {
- var generator = new SurgeConfigGenerator(proxyFile, geoSiteData);
- File.WriteAllText(cnPath, generator.GenerateChinaRuleSet());
- if (!silent) Console.WriteLine($"China rule set written to {cnPath}.");
- File.WriteAllText(globalPath, generator.GenerateGlobalRuleSet());
- if (!silent) Console.WriteLine($"Global rule set written to {globalPath}.");
- }
-
- public static void GenerateTo(string directory, string cnPath, string globalPath, bool clean, bool silent)
- {
- var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent);
- var proxyFile = new ProxyFile(Path.Combine(directory, ToolConfig.ProxyConfigFileName));
- var generator = new SurgeConfigGenerator(proxyFile, geoSiteData);
- File.WriteAllText(cnPath, generator.GenerateChinaRuleSet());
- if (!silent) Console.WriteLine($"China rule set written to {cnPath}.");
- File.WriteAllText(globalPath, generator.GenerateGlobalRuleSet());
- if (!silent) Console.WriteLine($"Global rule set written to {globalPath}.");
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs
deleted file mode 100644
index 1fe91b1..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs
+++ /dev/null
@@ -1,231 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text;
-
-namespace Crupest.SecretTool;
-
-public class Template
-{
- private enum ParseState
- {
- Text,
- Dollar,
- LeftBracket,
- VariableName,
- VariableNameFinish,
- }
-
- private interface ITemplateNode
- {
- string Render(Dictionary<string, string> values);
- }
-
- private class TextNode(string text) : ITemplateNode
- {
-
- public string Text { get; } = text;
-
- public string Render(Dictionary<string, string> values)
- {
- return Text;
- }
- }
-
- private class VariableNode(string variableName) : ITemplateNode
- {
- public string VariableName { get; } = variableName;
-
- public string Render(Dictionary<string, string> values)
- {
- return values.GetValueOrDefault(VariableName) ?? "";
- }
- }
-
- public Template(string templateString)
- {
- TemplateString = templateString;
- Nodes = Parse(templateString);
- VariableNames = Nodes.OfType<VariableNode>().Select(node => node.VariableName).ToList();
- }
-
- private static List<ITemplateNode> Parse(string templateString)
- {
- int lineNumber = 1;
- int columnNumber = 0;
- List<ITemplateNode> nodes = [];
- ParseState state = ParseState.Text;
- StringBuilder stringBuilder = new();
-
- string GetPosition() => $"line {lineNumber} column{columnNumber}";
-
- [DoesNotReturn]
- void ReportInvalidState(string message)
- {
- throw new Exception($"Invalid state at {GetPosition()}: {message}");
- }
-
- [DoesNotReturn]
- void ReportInvalidCharacter(char c)
- {
- throw new FormatException($"Unexpected '{c}' at {GetPosition()}.");
- }
-
- void FinishText()
- {
- if (state != ParseState.Text)
- {
- ReportInvalidState($"Can't call FinishText here.");
- }
-
- if (stringBuilder.Length > 0)
- {
- nodes.Add(new TextNode(stringBuilder.ToString()));
- stringBuilder.Clear();
- }
- }
-
- foreach (var c in templateString)
- {
- if (c == '\n')
- {
- lineNumber++;
- columnNumber = 0;
- }
-
- columnNumber++;
-
- switch (c)
- {
- case '$':
- if (state == ParseState.Text)
- {
- FinishText();
- state = ParseState.Dollar;
- }
- else if (state == ParseState.Dollar)
- {
- if (stringBuilder.Length > 0)
- {
- throw new Exception($"Invalid state at {GetPosition()}: when we meet the second '$', text builder should be empty.");
- }
- stringBuilder.Append(c);
- state = ParseState.Text;
- }
- else
- {
- throw new FormatException($"Unexpected '$' at {GetPosition()}.");
- }
- break;
- case '{':
- if (state == ParseState.Text)
- {
- stringBuilder.Append(c);
- }
- else if (state == ParseState.Dollar)
- {
- state = ParseState.LeftBracket;
- }
- else
- {
- throw new Exception($"Unexpected '{{' at {GetPosition()}.");
- }
- break;
- case '}':
- if (state == ParseState.Text)
- {
- stringBuilder.Append(c);
- state = ParseState.Text;
- }
- else if (state == ParseState.VariableName || state == ParseState.VariableNameFinish)
- {
- nodes.Add(new VariableNode(stringBuilder.ToString()));
- stringBuilder.Clear();
- state = ParseState.Text;
- }
- else
- {
- ReportInvalidCharacter(c);
- }
- break;
- default:
- if (state == ParseState.Dollar)
- {
- ReportInvalidCharacter(c);
- }
-
- if (char.IsWhiteSpace(c))
- {
- if (state == ParseState.LeftBracket || state == ParseState.VariableNameFinish)
- {
- continue;
- }
- else if (state == ParseState.Text)
- {
- stringBuilder.Append(c);
- }
- else if (state == ParseState.VariableName)
- {
- state = ParseState.VariableNameFinish;
- }
- else
- {
- ReportInvalidCharacter(c);
- }
- }
- else
- {
- if (state == ParseState.Text)
- {
- stringBuilder.Append(c);
- }
- else if (state == ParseState.LeftBracket || state == ParseState.VariableName)
- {
- stringBuilder.Append(c);
- state = ParseState.VariableName;
- }
- else
- {
- ReportInvalidCharacter(c);
- }
- }
- break;
- }
- }
-
- if (state == ParseState.Text)
- {
- FinishText();
- }
- else
- {
- throw new FormatException("Unexpected end of template string.");
- }
-
- return nodes;
- }
-
- public string TemplateString { get; }
- private List<ITemplateNode> Nodes { get; set; }
- public List<string> VariableNames { get; }
-
- public string Generate(Dictionary<string, string> values, bool allowMissingVariable = false)
- {
- StringBuilder stringBuilder = new();
- foreach (var node in Nodes)
- {
- if (node is TextNode textNode)
- {
- stringBuilder.Append(textNode.Text);
- }
- else if (node is VariableNode variableNode)
- {
- var hasValue = values.TryGetValue(variableNode.VariableName, out var value);
- if (!hasValue && !allowMissingVariable)
- {
- throw new Exception($"Variable '{variableNode.VariableName}' is not set.");
- }
- stringBuilder.Append(hasValue ? value : string.Empty);
- }
- }
- return stringBuilder.ToString();
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs
deleted file mode 100644
index 809fba1..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs
+++ /dev/null
@@ -1,271 +0,0 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Crupest.SecretTool;
-
-public interface IV4ConfigObject
-{
- object ToJsonObjectV4();
-}
-
-public interface ISingConfigObject
-{
- object ToJsonObjectSing();
-}
-
-public class ToolConfigBase(Template template, List<Proxy> proxies, Routing router)
-{
- protected class JsonInterfaceConverter<Interface> : JsonConverter<Interface>
- {
- public override Interface Read(
- ref Utf8JsonReader reader,
- Type typeToConvert,
- JsonSerializerOptions options)
- {
- throw new NotImplementedException();
- }
-
- public override void Write(
- Utf8JsonWriter writer,
- Interface value,
- JsonSerializerOptions options)
- {
- JsonSerializer.Serialize(writer, value, typeof(object), options);
- }
- }
-
- public const string VmessConfigFileName = "vmess.txt";
- public const string ProxyConfigFileName = "proxy.txt";
-
- public Template Template { get; set; } = template;
- public List<Proxy> Proxies { get; set; } = proxies;
- public Routing Routing { get; set; } = router;
-}
-
-public class ToolConfig(Template template, List<Proxy> proxies, Routing router, StaticHosts? hosts) : ToolConfigBase(template, proxies, router)
-{
- public const string ConfigTemplateFileName = "config.json.template";
- public const string HostsConfigFileName = "hosts.txt";
-
- public static List<string> RequiredConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName];
- public static List<string> ConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName, HostsConfigFileName];
-
- private const string ProxyAnchor = "PROXY_ANCHOR";
- private const string RoutingAnchor = "ROUTING_ANCHOR";
- private const string HostsAnchor = "HOSTS_ANCHOR";
-
- public const string AddCnAttributeToGeositeEnvironmentVariable = "CRUPEST_V2RAY_GEOSITE_USE_CN";
-
- private static bool UseCnGeoSite => Environment.GetEnvironmentVariable(AddCnAttributeToGeositeEnvironmentVariable) switch
- {
- "0" or "false" or "off" or "disable" => false,
- _ => true
- };
-
- public StaticHosts Hosts { get; set; } = hosts is null ? new StaticHosts([]) : hosts;
-
- public string ToJsonStringV4(string domainStrategy = "IpOnDemand", bool directGeositeCn = true, bool pretty = true)
- {
- var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- });
- // TODO: Make interface converter generic.
- jsonOptions.Converters.Add(new JsonInterfaceConverter<V4ConfigJsonObjects.IOutboundSettings>());
- jsonOptions.Converters.Add(new JsonInterfaceConverter<V4ConfigJsonObjects.IOutboundStreamSettings>());
-
- var templateValues = new Dictionary<string, string>
- {
- [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectV4(), jsonOptions))),
- [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(domainStrategy, directGeositeCn), jsonOptions),
- [HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObjectV4(), jsonOptions),
- };
-
- var configString = Template.Generate(templateValues);
-
- if (pretty)
- {
- var jsonOptionsPretty = new JsonSerializerOptions(jsonOptions)
- {
- WriteIndented = true,
- };
- return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(configString, jsonOptionsPretty), jsonOptionsPretty);
- }
- else
- {
- return configString;
- }
- }
-
- public static ToolConfig FromFiles(string templatePath, string vmessPath, string proxyPath, string? hostsPath)
- {
- foreach (var path in new List<string>([templatePath, vmessPath, proxyPath]))
- {
- if (!File.Exists(path))
- {
- throw new FileNotFoundException($"Required config file not found: {path}.");
- }
- }
-
- ProxyFile proxyFile = new(proxyPath);
- string templateString, vmessString;
- string? hostsString;
-
- string file = "";
- try
- {
- file = templatePath;
- templateString = File.ReadAllText(templatePath);
- file = vmessPath;
- vmessString = File.ReadAllText(vmessPath);
- hostsString = hostsPath is not null ? File.ReadAllText(hostsPath) : null;
- }
- catch (Exception e)
- {
- throw new Exception($"Error reading config file {file}.", e);
- }
-
- try
- {
- file = templatePath;
- var template = new Template(templateString);
- file = vmessPath;
- var vmess = VmessProxy.CreateFromConfigString(vmessString, "proxy");
- file = proxyPath;
- var routing = Routing.FromProxyFile(proxyFile, "proxy");
- file = hostsPath ?? "";
- var hosts = hostsString is not null ? StaticHosts.CreateFromHostMatchConfigString(hostsString) : null;
- return new ToolConfig(template, [vmess], routing, hosts);
- }
- catch (Exception e)
- {
- throw new Exception($"Error parsing config file {file}.", e);
- }
- }
-
- public static ToolConfig FromDirectory(string directory)
- {
- return FromFiles(
- Path.Join(directory, ConfigTemplateFileName),
- Path.Join(directory, VmessConfigFileName),
- Path.Join(directory, ProxyConfigFileName),
- Path.Join(directory, HostsConfigFileName)
- );
- }
-
- public static void FromDirectoryAndWriteToFile(string directory, string outputPath)
- {
- var config = FromDirectory(directory);
- File.WriteAllText(outputPath, config.ToJsonStringV4());
- }
-}
-
-public class SingToolConfig(Template template, List<Proxy> proxies, Routing router, string inboundsString) : ToolConfigBase(template, proxies, router)
-{
-
- public const string ConfigTemplateFileName = "sing-config.json.template";
- public const string ConfigInboundsPcFileName = "sing-inbounds-pc.json";
- public const string ConfigInboundsMobileFileName = "sing-inbounds-mobile.json";
-
- public static List<string> RequiredConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName, ConfigInboundsMobileFileName, ConfigInboundsPcFileName];
-
- private const string ProxyAnchor = "PROXY_ANCHOR";
- private const string RouteAnchor = "ROUTE_ANCHOR";
- private const string InboundsAnchor = "INBOUNDS_ANCHOR";
-
- public string InboundsString { get; } = inboundsString;
-
- public string ToSingConfigString(bool pretty = true)
- {
- var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
- DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- });
- // TODO: Make interface converter generic.
- jsonOptions.Converters.Add(new JsonInterfaceConverter<SingConfigJsonObjects.OutboundBase>());
- jsonOptions.Converters.Add(new JsonInterfaceConverter<SingConfigJsonObjects.V2rayTransportBase>());
-
- var templateValues = new Dictionary<string, string>
- {
- [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectSing(), jsonOptions))),
- [RouteAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectSing(), jsonOptions),
- [InboundsAnchor] = InboundsString
- };
-
- var configString = Template.Generate(templateValues);
-
- if (pretty)
- {
- var jsonOptionsPretty = new JsonSerializerOptions(jsonOptions)
- {
- WriteIndented = true,
- };
- return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(configString, jsonOptionsPretty), jsonOptionsPretty);
- }
- else
- {
- return configString;
- }
- }
-
- public static SingToolConfig FromFiles(string templatePath, string vmessPath, string proxyPath, string inboundsPath, bool clean, bool silent)
- {
- foreach (var path in new List<string>([templatePath, vmessPath, proxyPath, inboundsPath]))
- {
- if (!File.Exists(path))
- {
- throw new FileNotFoundException($"Required config file not found: {path}.");
- }
- }
-
- var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent);
-
- ProxyFile proxyFile = new(proxyPath);
- string templateString, vmessString, inboundsString;
-
- string file = "";
- try
- {
- file = templatePath;
- templateString = File.ReadAllText(templatePath);
- file = vmessPath;
- vmessString = File.ReadAllText(vmessPath);
- file = inboundsPath;
- inboundsString = File.ReadAllText(inboundsPath);
- }
- catch (Exception e)
- {
- throw new Exception($"Error reading config file {file}.", e);
- }
-
- try
- {
- file = templatePath;
- var template = new Template(templateString);
- file = vmessPath;
- var vmess = VmessProxy.CreateFromConfigString(vmessString, "proxy-out");
- file = proxyPath;
- var routing = Routing.FromProxyFileForSing(proxyFile, geoSiteData, "proxy-out", "direct-out");
- return new SingToolConfig(template, [vmess], routing, inboundsString);
- }
- catch (Exception e)
- {
- throw new Exception($"Error parsing config file {file}.", e);
- }
- }
-
- public static SingToolConfig FromDirectory(string directory, bool isMobile, bool clean, bool silent)
- {
- return FromFiles(
- Path.Join(directory, ConfigTemplateFileName),
- Path.Join(directory, VmessConfigFileName),
- Path.Join(directory, ProxyConfigFileName),
- isMobile ? Path.Join(directory, ConfigInboundsMobileFileName) : Path.Join(directory, ConfigInboundsPcFileName),
- clean, silent
- );
- }
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs
deleted file mode 100644
index 3e81dbb..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace Crupest.SecretTool;
-
-public static class V4ConfigJsonObjects
-{
- public interface IObject;
- public interface IOutboundSettings : IObject;
- public interface IOutboundStreamSettings : IObject;
-
- public record WsSettings(string Path, Dictionary<string, string> Headers) : IObject;
- public record WsStreamSettings(string Network, string Security, WsSettings WsSettings) : IOutboundStreamSettings;
- public record VnextServerUser(string Id, int AlterId, string Security, int Level) : IObject;
- public record VnextServer(string Address, int Port, List<VnextServerUser> Users) : IObject;
- public record VmessOutboundSettings(List<VnextServer> Vnext) : IOutboundSettings;
- public record HttpOutboundUser(string User, string Pass) : IObject;
- public record HttpOutboundServer(string Address, int Port, List<HttpOutboundUser> Users) : IObject;
- public record HttpOutboundSettings(List<HttpOutboundServer> Servers) : IOutboundSettings;
- public record Outbound(string Tag, string Protocol, IOutboundSettings Settings,
- IOutboundStreamSettings? StreamSettings) : IObject;
-
- public record RoutingRule(string DomainMatcher = "mph", string Type = "field", List<string>? Domains = null, List<string>? Ip = null,
- string? Port = null, string? SourcePort = null, string? Network = null, List<string>? Source = null,
- List<string>? User = null, List<string>? InboundTag = null, List<string>? Protocol = null, string? Attrs = null,
- string? OutboundTag = null, string? BalancerTag = null) : IObject;
- public record Routing(List<RoutingRule> Rules, string DomainStrategy = "IpOnDemand", string DomainMatcher = "mph") : IObject;
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs
deleted file mode 100644
index a50e9be..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace Crupest.SecretTool;
-
-public static class V5ConfigJsonObjects
-{
- public record OutboundObject(string Protocol, object Settings, string Tag, object? StreamSettings)
- {
- public static OutboundObject VmessViaWs(string tag, string address, int port, string uuid, string path)
- {
- return new OutboundObject("vmess", new VmessSettings(address, port, uuid), tag, StreamSettingsObject.Ws(path));
- }
-
- public static OutboundObject Http(string tag, string address, int port)
- {
- return new OutboundObject("http", new HttpSettingsObject(address, port), tag, null);
- }
- }
-
- public record WsSettingsObject(string Path, Dictionary<string, string> Headers);
-
- public record StreamSettingsObject(string Transport, object TransportSettings, string Security, object SecuritySettings)
- {
- public static StreamSettingsObject Ws(string path)
- {
- return new StreamSettingsObject("ws", new WsSettingsObject(path, new()), "tls", new());
- }
- }
-
- public record VmessSettings(string Address, int Port, string Uuid);
-
- public record HttpSettingsObject(string Address, int Port);
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/config.json.template b/store/works/Crupest.SecretTool/Crupest.SecretTool/config.json.template
deleted file mode 100644
index 424e996..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/config.json.template
+++ /dev/null
@@ -1,63 +0,0 @@
-{
- "log": {
- "loglevel": "warning"
- },
- "inbounds": [
- {
- "port": 2081,
- "listen": "127.0.0.1",
- "tag": "socks-inbound",
- "protocol": "socks",
- "settings": {
- "auth": "noauth"
- }
- },
- {
- "port": 2080,
- "listen": "127.0.0.1",
- "tag": "http-inbound",
- "protocol": "http",
- "settings": {
- "auth": "noauth"
- }
- }
- ],
- "outbounds": [
- {
- "protocol": "freedom",
- "settings": {},
- "tag": "direct"
- },
- {
- "protocol": "blackhole",
- "settings": {},
- "tag": "blocked"
- },
- ${PROXY_ANCHOR}
- ],
- "routing": ${ROUTING_ANCHOR},
- "dns": {
- "hosts": ${HOSTS_ANCHOR},
- "servers": [
- "https://doh.pub/dns-query",
- "1.1.1.1",
- "8.8.8.8",
- "localhost"
- ]
- },
- "policy": {
- "levels": {
- "0": {
- "uplinkOnly": 0,
- "downlinkOnly": 0
- }
- },
- "system": {
- "statsInboundUplink": false,
- "statsInboundDownlink": false,
- "statsOutboundUplink": false,
- "statsOutboundDownlink": false
- }
- },
- "other": {}
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template b/store/works/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template
deleted file mode 100644
index 01ccf7a..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template
+++ /dev/null
@@ -1,55 +0,0 @@
-{
- "log": {
- "access": {
- "type": "Console",
- "level": "Info"
- }
- },
- "dns": {
- "nameServer": [{
- "address": "https://doh.pub/dns-query"
- }, {
- "address": "1.1.1.1"
- }, {
- "address": "8.8.8.8"
- }, {
- "address": "localhost"
- }],
- "staticHosts": ${HOSTS_ANCHOR}
- },
- "inbounds": [{
- {
- "protocol": "socks",
- "port": 2081,
- "listen": "127.0.0.1",
- "tag": "socks-inbound",
- "settings": {
- "auth": "noauth"
- }
- },
- {
- "protocol": "http",
- "port": 2080,
- "listen": "127.0.0.1",
- "tag": "http-inbound",
- "settings": {
- "auth": "noauth"
- }
- }
- }],
- "outbounds": [
- {
- "protocol": "freedom",
- "settings": {},
- "tag": "direct"
- },
- {
- "protocol": "blackhole",
- "settings": {},
- "tag": "blocked"
- },
- ${PROXY_ANCHOR}
- ],
- "router": ${ROUTER_ANCHOR}
-}
-
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/hosts.txt b/store/works/Crupest.SecretTool/Crupest.SecretTool/hosts.txt
deleted file mode 100644
index 88d5015..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/hosts.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-cdn.jsdelivr.net cdn.jsdelivr.net.cdn.cloudflare.net
-
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/proxy.txt b/store/works/Crupest.SecretTool/Crupest.SecretTool/proxy.txt
deleted file mode 100644
index 39800f9..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/proxy.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-GeoSite microsoft
-GeoSite google
-GeoSite youtube
-GeoSite x
-GeoSite facebook
-GeoSite discord
-GeoSite reddit
-GeoSite twitch
-GeoSite quora
-GeoSite telegram
-GeoSite imgur
-GeoSite stackexchange
-GeoSite medium
-
-GeoSite duckduckgo
-GeoSite wikimedia
-GeoSite gitbook
-GeoSite github
-GeoSite gitlab
-GeoSite sourceforge
-GeoSite creativecommons
-GeoSite archive
-GeoSite matrix
-GeoSite tor
-
-GeoSite python
-GeoSite ruby
-GeoSite rust
-GeoSite nodejs
-GeoSite npmjs
-GeoSite qt
-GeoSite docker
-GeoSite v2ray
-GeoSite homebrew
-
-GeoSite azure
-GeoSite akamai
-GeoSite aws
-GeoSite jsdelivr
-GeoSite fastly
-GeoSite heroku
-GeoSite bootstrap
-GeoSite vercel
-
-GeoSite ieee
-GeoSite sci-hub
-GeoSite libgen
-GeoSite z-library
-
-sagernet.org
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template b/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template
deleted file mode 100644
index d7e55a0..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "log": {
- "disabled": false,
- "level": "info",
- "timestamp": true
- },
- "dns": {
- "servers": [
- {
- "tag": "ali-doh",
- "address": "https://dns.alidns.com/dns-query",
- "address_resolver": "ali"
- },
- {
- "tag": "ali",
- "address": "223.5.5.5"
- },
- {
- "tag": "cloudflare",
- "address": "1.1.1.1"
- },
- {
- "tag": "google",
- "address": "8.8.8.8"
- }
- ]
- },
- "inbounds": ${INBOUNDS_ANCHOR},
- "outbounds": [
- {
- "type": "direct",
- "tag": "direct-out"
- },
- {
- "type": "block",
- "tag": "block-out"
- },
- {
- "tag": "dns-out",
- "type": "dns"
- },
- ${PROXY_ANCHOR}
- ],
- "route": ${ROUTE_ANCHOR}
-}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-mobile.json b/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-mobile.json
deleted file mode 100644
index 5038c40..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-mobile.json
+++ /dev/null
@@ -1,11 +0,0 @@
-[
- {
- "tag": "tun-in",
- "type": "tun",
- "auto_route": true,
- "strict_route": true,
- "address": [ "172.23.0.1/30", "fdfe:acbd:9876::1/126"],
- "sniff": true,
- "sniff_override_destination": true
- }
-]
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-pc.json b/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-pc.json
deleted file mode 100644
index 956d751..0000000
--- a/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-pc.json
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "tag": "http-in",
- "type": "http",
- "listen": "127.0.0.1",
- "listen_port": 3080
- },
- {
- "tag": "socks-in",
- "type": "socks",
- "listen": "127.0.0.1",
- "listen_port": 3081
- }
-] \ No newline at end of file
diff --git a/store/works/Crupest.SecretTool/build-secret.bash b/store/works/Crupest.SecretTool/build-secret.bash
deleted file mode 100755
index 8878049..0000000
--- a/store/works/Crupest.SecretTool/build-secret.bash
+++ /dev/null
@@ -1,41 +0,0 @@
-#! /usr/bin/env bash
-
-set -e
-
-function print_argument_error_message_and_exit() {
- argument_error_message="You must specify exactly one argument, the build target (win-x64 | linux-x64 | osx-x64)."
- echo "$argument_error_message"
- exit 1
-}
-
-
-
-if [[ $# != 1 ]]; then
- print_argument_error_message_and_exit
-fi
-
-case "$1" in
- win-x64 | linux-x64 | osx-x64)
- echo "Build target: $1"
- ;;
- *)
- print_argument_error_message_and_exit
- ;;
-esac
-
-secret_dir=$(realpath "$(dirname "$0")")
-
-echo "Secret dir: ${secret_dir}"
-
-echo "Check dotnet..."
-dotnet --version
-
-echo "Enter \"secret\" dir..."
-pushd "$secret_dir"
-
-echo "Begin to build..."
-dotnet publish Crupest.SecretTool -c Release -o "$secret_dir/publish" --sc -r "$1"
-
-popd
-
-echo "Finish!"
diff --git a/store/works/Crupest.SecretTool/build-secret.ps1 b/store/works/Crupest.SecretTool/build-secret.ps1
deleted file mode 100644
index 8aa7987..0000000
--- a/store/works/Crupest.SecretTool/build-secret.ps1
+++ /dev/null
@@ -1,25 +0,0 @@
-if ($args.Count -ne 1 || $args[0] -notmatch "^win-x64|linux-x64|osx-x64$")
-{
- Write-Error "You must specify exactly one argument, the build target (win-x64 | linux-x64 | osx-x64)."
- exit 1
-}
-
-Write-Output "Secret dir: $PSScriptRoot"
-
-Write-Output "Check dotnet..."
-dotnet --version
-if ($LASTEXITCODE -ne 0)
-{
- Write-Error "dotnet not found."
- exit 2
-}
-
-Write-Output "Enter `"secret`" dir..."
-Push-Location $PSScriptRoot
-
-Write-Output "Begin to build..."
-dotnet publish Crupest.SecretTool -c Release -o "$secret_dir/publish" --sc -r $args[0]
-
-Pop-Location
-
-Write-Host "Finish!" -ForegroundColor Green
diff --git a/store/works/Crupest.SecretTool/tools/cru-proxy-edit b/store/works/Crupest.SecretTool/tools/cru-proxy-edit
deleted file mode 100755
index 51a33e1..0000000
--- a/store/works/Crupest.SecretTool/tools/cru-proxy-edit
+++ /dev/null
@@ -1,12 +0,0 @@
-#! /usr/bin/env bash
-
-set -e
-
-p="$HOME/codes/crupest/tools/Crupest.SecretTool/publish/proxy.txt"
-
-if [[ ! -f "$p" ]]; then
- echo "File $p does not exist!" >&2
- exit 1
-fi
-
-exec vim "$p"
diff --git a/store/works/Crupest.SecretTool/tools/cru-proxy-log b/store/works/Crupest.SecretTool/tools/cru-proxy-log
deleted file mode 100755
index 6ec6ee1..0000000
--- a/store/works/Crupest.SecretTool/tools/cru-proxy-log
+++ /dev/null
@@ -1,13 +0,0 @@
-#! /usr/bin/env bash
-
-set -e
-
-if [[ -e /proc ]]; then
- # I don't believe your system is Linux but there is no /proc.
- exec journalctl --user -u crupest-secret-tool "$@"
-elif [[ "$(uname)" == "Darwin" ]]; then
- exec less "$HOME/.local/state/Crupest.SecretTool/log"
-else
- echo "Not supported on systems other than macOS and Linux now." >&2
- exit 1
-fi
diff --git a/store/works/Crupest.SecretTool/tools/crupest-secret-tool.service b/store/works/Crupest.SecretTool/tools/crupest-secret-tool.service
deleted file mode 100644
index df6d172..0000000
--- a/store/works/Crupest.SecretTool/tools/crupest-secret-tool.service
+++ /dev/null
@@ -1,8 +0,0 @@
-[Unit]
-Description=crupest v2ray service
-
-[Service]
-ExecStart=%h/.local/bin/Crupest.SecretTool
-
-[Install]
-WantedBy=default.target
diff --git a/store/works/Crupest.SecretTool/tools/crupest-secret-tool.xml b/store/works/Crupest.SecretTool/tools/crupest-secret-tool.xml
deleted file mode 100644
index 9b85f13..0000000
--- a/store/works/Crupest.SecretTool/tools/crupest-secret-tool.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-<!--
- MIT License
-
- Copyright (c) 2008-2020 Kohsuke Kawaguchi, Sun Microsystems, Inc., CloudBees,
- Inc., Oleg Nenashev and other contributors
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
--->
-
-<!--
- This is a sample configuration of the Windows Service Wrapper.
- This configuration file should be placed near the WinSW executable, the name should be the same.
- E.g. for myapp.exe the configuration file name should be myapp.xml
-
- You can find more information about configuration options here:
-https://github.com/kohsuke/winsw/blob/master/doc/xmlConfigFile.md
--->
-<service>
- <id>crupest-secret-tool</id>
- <name>Crupest Secret Tool</name>
- <description>Crupest Secret Tool (powered by WinSW)</description>
-
- <!-- Path to the executable, which should be started -->
- <executable>%BASE%\Crupest.SecretTool.exe</executable>
-
- <onfailure action="restart" delay="10 sec" />
- <onfailure action="restart" delay="30 sec" />
- <onfailure action="restart" delay="50 sec" />
-
- <workingdirectory>%BASE%</workingdirectory>
-
- <startmode>Automatic</startmode>
-</service> \ No newline at end of file
diff --git a/store/works/Crupest.SecretTool/tools/life.crupest.secret-tool.plist b/store/works/Crupest.SecretTool/tools/life.crupest.secret-tool.plist
deleted file mode 100644
index bdfe490..0000000
--- a/store/works/Crupest.SecretTool/tools/life.crupest.secret-tool.plist
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>Label</key>
- <string>life.crupest.secret-tool</string>
- <key>ProgramArguments</key>
- <array>
- <string>/Users/crupest/.local/bin/Crupest.SecretTool</string>
- </array>
- <key>KeepAlive</key>
- <true/>
- <key>StandardOutPath</key>
- <string>/Users/crupest/.local/state/Crupest.SecretTool/log</string>
- <key>StandardErrorPath</key>
- <string>/Users/crupest/.local/state/Crupest.SecretTool/error</string>
-</dict>
-</plist>
diff --git a/store/works/README.md b/store/works/README.md
deleted file mode 100644
index b280870..0000000
--- a/store/works/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-This directory contains some of my works that I have created. I moved them from individual repositories to here for easier management. Some works are stopped for its temporary purpose. Here are some of the descriptions of them.
-
-- `life`: Something I created in university time. Some for lecture tasks, some are small toys to solve some interesting problems. Maybe more will be added in future.
-
-- `solutions`: Answers to leetcode and acwing algorithms questions. Stopped now for a well-known reason. Maybe more will be added in future, too.
-
-- `teapot`: Answer to the pre-interview test for my first job. Dropped now definitely.
-
-- `ProxyChanger`: Written a long time ago to solve the proxy switching problem in Firefox for a also well-known problem.
diff --git a/store/works/bruno/ComfyUI/Get Object Info.bru b/store/works/bruno/ComfyUI/Get Object Info.bru
deleted file mode 100644
index d1a833c..0000000
--- a/store/works/bruno/ComfyUI/Get Object Info.bru
+++ /dev/null
@@ -1,11 +0,0 @@
-meta {
- name: Get Object Info
- type: http
- seq: 4
-}
-
-get {
- url: {{BASE_URL}}/object_info
- body: none
- auth: none
-}
diff --git a/store/works/bruno/ComfyUI/Get Prompt History.bru b/store/works/bruno/ComfyUI/Get Prompt History.bru
deleted file mode 100644
index 2e26888..0000000
--- a/store/works/bruno/ComfyUI/Get Prompt History.bru
+++ /dev/null
@@ -1,15 +0,0 @@
-meta {
- name: Get Prompt History
- type: http
- seq: 6
-}
-
-get {
- url: {{BASE_URL}}/history/{{prompt_id}}
- body: none
- auth: none
-}
-
-vars:pre-request {
- prompt_id: 7e345a55-21c4-4bdc-9b34-add561775144
-}
diff --git a/store/works/bruno/ComfyUI/Post Prompt.bru b/store/works/bruno/ComfyUI/Post Prompt.bru
deleted file mode 100644
index 09bf89a..0000000
--- a/store/works/bruno/ComfyUI/Post Prompt.bru
+++ /dev/null
@@ -1,124 +0,0 @@
-meta {
- name: Post Prompt
- type: http
- seq: 5
-}
-
-post {
- url: {{BASE_URL}}/prompt
- body: json
- auth: none
-}
-
-body:json {
- {
- "client_id": "crupest",
- "prompt": {
- "3": {
- "inputs": {
- "seed": 156680208700286,
- "steps": 20,
- "cfg": 8,
- "sampler_name": "euler",
- "scheduler": "normal",
- "denoise": 1,
- "model": [
- "4",
- 0
- ],
- "positive": [
- "6",
- 0
- ],
- "negative": [
- "7",
- 0
- ],
- "latent_image": [
- "5",
- 0
- ]
- },
- "class_type": "KSampler",
- "_meta": {
- "title": "KSampler"
- }
- },
- "4": {
- "inputs": {
- "ckpt_name": "SUPIR/SUPIR-v0Q.ckpt"
- },
- "class_type": "CheckpointLoaderSimple",
- "_meta": {
- "title": "Load Checkpoint"
- }
- },
- "5": {
- "inputs": {
- "width": 512,
- "height": 512,
- "batch_size": 1
- },
- "class_type": "EmptyLatentImage",
- "_meta": {
- "title": "Empty Latent Image"
- }
- },
- "6": {
- "inputs": {
- "text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
- "clip": [
- "4",
- 1
- ]
- },
- "class_type": "CLIPTextEncode",
- "_meta": {
- "title": "CLIP Text Encode (Prompt)"
- }
- },
- "7": {
- "inputs": {
- "text": "text, watermark",
- "clip": [
- "4",
- 1
- ]
- },
- "class_type": "CLIPTextEncode",
- "_meta": {
- "title": "CLIP Text Encode (Prompt)"
- }
- },
- "8": {
- "inputs": {
- "samples": [
- "3",
- 0
- ],
- "vae": [
- "4",
- 2
- ]
- },
- "class_type": "VAEDecode",
- "_meta": {
- "title": "VAE Decode"
- }
- },
- "9": {
- "inputs": {
- "filename_prefix": "ComfyUI",
- "images": [
- "8",
- 0
- ]
- },
- "class_type": "SaveImage",
- "_meta": {
- "title": "Save Image"
- }
- }
- }
- }
-}
diff --git a/store/works/bruno/ComfyUI/Upload Image.bru b/store/works/bruno/ComfyUI/Upload Image.bru
deleted file mode 100644
index 92b4aeb..0000000
--- a/store/works/bruno/ComfyUI/Upload Image.bru
+++ /dev/null
@@ -1,18 +0,0 @@
-meta {
- name: Upload Image
- type: http
- seq: 2
-}
-
-post {
- url: {{BASE_URL}}/upload/image
- body: multipartForm
- auth: none
-}
-
-body:multipart-form {
- overwrite: true
- type: input
- subfolder: crupest-test
- image: @file(/Users/crupest/codes/crupest/assets/crupest-transparent.png)
-}
diff --git a/store/works/bruno/ComfyUI/View Image.bru b/store/works/bruno/ComfyUI/View Image.bru
deleted file mode 100644
index 395eccd..0000000
--- a/store/works/bruno/ComfyUI/View Image.bru
+++ /dev/null
@@ -1,19 +0,0 @@
-meta {
- name: View Image
- type: http
- seq: 1
-}
-
-get {
- url: {{BASE_URL}}/view?filename=crupest-transparent.png&type=input&subfolder=crupest-test&preview=jpeg;90&channel=rgb
- body: none
- auth: none
-}
-
-query {
- filename: crupest-transparent.png
- type: input
- subfolder: crupest-test
- preview: jpeg;90
- channel: rgb
-}
diff --git a/store/works/bruno/ComfyUI/bruno.json b/store/works/bruno/ComfyUI/bruno.json
deleted file mode 100644
index ee35540..0000000
--- a/store/works/bruno/ComfyUI/bruno.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "version": "1",
- "name": "ComfyUI",
- "type": "collection",
- "ignore": [
- "node_modules",
- ".git"
- ]
-} \ No newline at end of file
diff --git a/store/works/bruno/ComfyUI/environments/ChimerAI ComfyUI Server.bru b/store/works/bruno/ComfyUI/environments/ChimerAI ComfyUI Server.bru
deleted file mode 100644
index 480c8da..0000000
--- a/store/works/bruno/ComfyUI/environments/ChimerAI ComfyUI Server.bru
+++ /dev/null
@@ -1,3 +0,0 @@
-vars:secret [
- BASE_URL
-]
diff --git a/store/works/python/.gitignore b/store/works/python/.gitignore
deleted file mode 100644
index f5833b1..0000000
--- a/store/works/python/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-__pycache__
-.venv
-.mypy_cache
diff --git a/store/works/python/.python-version b/store/works/python/.python-version
deleted file mode 100644
index 2c07333..0000000
--- a/store/works/python/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.11
diff --git a/store/works/python/cru/__init__.py b/store/works/python/cru/__init__.py
deleted file mode 100644
index 17799a9..0000000
--- a/store/works/python/cru/__init__.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import sys
-
-from ._base import CRU, CruNamespaceError, CRU_NAME_PREFIXES
-from ._error import (
- CruException,
- CruLogicError,
- CruInternalError,
- CruUnreachableError,
- cru_unreachable,
-)
-from ._const import (
- CruConstantBase,
- CruDontChange,
- CruNotFound,
- CruNoValue,
- CruPlaceholder,
- CruUseDefault,
-)
-from ._func import CruFunction
-from ._iter import CruIterable, CruIterator
-from ._event import CruEvent, CruEventHandlerToken
-from ._type import CruTypeSet, CruTypeCheckError
-
-
-class CruInitError(CruException):
- pass
-
-
-def check_python_version(required_version=(3, 11)):
- if sys.version_info < required_version:
- raise CruInitError(f"Python version must be >= {required_version}!")
-
-
-check_python_version()
-
-__all__ = [
- "CRU",
- "CruNamespaceError",
- "CRU_NAME_PREFIXES",
- "check_python_version",
- "CruException",
- "CruInternalError",
- "CruLogicError",
- "CruUnreachableError",
- "cru_unreachable",
- "CruInitError",
- "CruConstantBase",
- "CruDontChange",
- "CruNotFound",
- "CruNoValue",
- "CruPlaceholder",
- "CruUseDefault",
- "CruFunction",
- "CruIterable",
- "CruIterator",
- "CruEvent",
- "CruEventHandlerToken",
- "CruTypeSet",
- "CruTypeCheckError",
-]
diff --git a/store/works/python/cru/_base.py b/store/works/python/cru/_base.py
deleted file mode 100644
index 2599d8f..0000000
--- a/store/works/python/cru/_base.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from typing import Any
-
-from ._helper import remove_none
-from ._error import CruException
-
-
-class CruNamespaceError(CruException):
- """Raised when a namespace is not found."""
-
-
-class _Cru:
- NAME_PREFIXES = ("CRU_", "Cru", "cru_")
-
- def __init__(self) -> None:
- self._d: dict[str, Any] = {}
-
- def all_names(self) -> list[str]:
- return list(self._d.keys())
-
- def get(self, name: str) -> Any:
- return self._d[name]
-
- def has_name(self, name: str) -> bool:
- return name in self._d
-
- @staticmethod
- def _maybe_remove_prefix(name: str) -> str | None:
- for prefix in _Cru.NAME_PREFIXES:
- if name.startswith(prefix):
- return name[len(prefix) :]
- return None
-
- def _check_name_exist(self, *names: str | None) -> None:
- for name in names:
- if name is None:
- continue
- if self.has_name(name):
- raise CruNamespaceError(f"Name {name} exists in CRU.")
-
- @staticmethod
- def check_name_format(name: str) -> tuple[str, str]:
- no_prefix_name = _Cru._maybe_remove_prefix(name)
- if no_prefix_name is None:
- raise CruNamespaceError(
- f"Name {name} is not prefixed with any of {_Cru.NAME_PREFIXES}."
- )
- return name, no_prefix_name
-
- @staticmethod
- def _check_object_name(o) -> tuple[str, str]:
- return _Cru.check_name_format(o.__name__)
-
- def _do_add(self, o, *names: str | None) -> list[str]:
- name_list: list[str] = remove_none(names)
- for name in name_list:
- self._d[name] = o
- return name_list
-
- def add(self, o, name: str | None) -> tuple[str, str | None]:
- no_prefix_name: str | None
- if name is None:
- name, no_prefix_name = self._check_object_name(o)
- else:
- no_prefix_name = self._maybe_remove_prefix(name)
-
- self._check_name_exist(name, no_prefix_name)
- self._do_add(o, name, no_prefix_name)
- return name, no_prefix_name
-
- def add_with_alias(self, o, name: str | None = None, *aliases: str) -> list[str]:
- final_names: list[str | None] = []
- no_prefix_name: str | None
- if name is None:
- name, no_prefix_name = self._check_object_name(o)
- self._check_name_exist(name, no_prefix_name)
- final_names.extend([name, no_prefix_name])
- for alias in aliases:
- no_prefix_name = self._maybe_remove_prefix(alias)
- self._check_name_exist(alias, no_prefix_name)
- final_names.extend([alias, no_prefix_name])
-
- return self._do_add(o, *final_names)
-
- def add_objects(self, *objects):
- final_list = []
- for o in objects:
- name, no_prefix_name = self._check_object_name(o)
- self._check_name_exist(name, no_prefix_name)
- final_list.append((o, name, no_prefix_name))
- for o, name, no_prefix_name in final_list:
- self._do_add(o, name, no_prefix_name)
-
- def __getitem__(self, item):
- return self.get(item)
-
- def __getattr__(self, item):
- return self.get(item)
-
-
-CRU_NAME_PREFIXES = _Cru.NAME_PREFIXES
-CRU = _Cru()
diff --git a/store/works/python/cru/_const.py b/store/works/python/cru/_const.py
deleted file mode 100644
index 8246b35..0000000
--- a/store/works/python/cru/_const.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from enum import Enum, auto
-from typing import Self, TypeGuard, TypeVar
-
-from ._base import CRU
-
-_T = TypeVar("_T")
-
-
-class CruConstantBase(Enum):
- @classmethod
- def check(cls, v: _T | Self) -> TypeGuard[Self]:
- return isinstance(v, cls)
-
- @classmethod
- def check_not(cls, v: _T | Self) -> TypeGuard[_T]:
- return not cls.check(v)
-
- @classmethod
- def value(cls) -> Self:
- return cls.VALUE # type: ignore
-
-
-class CruNotFound(CruConstantBase):
- VALUE = auto()
-
-
-class CruUseDefault(CruConstantBase):
- VALUE = auto()
-
-
-class CruDontChange(CruConstantBase):
- VALUE = auto()
-
-
-class CruNoValue(CruConstantBase):
- VALUE = auto()
-
-
-class CruPlaceholder(CruConstantBase):
- VALUE = auto()
-
-
-CRU.add_objects(
- CruNotFound,
- CruUseDefault,
- CruDontChange,
- CruNoValue,
- CruPlaceholder,
-)
diff --git a/store/works/python/cru/_decorator.py b/store/works/python/cru/_decorator.py
deleted file mode 100644
index 137fc05..0000000
--- a/store/works/python/cru/_decorator.py
+++ /dev/null
@@ -1,97 +0,0 @@
-from __future__ import annotations
-
-from collections.abc import Callable
-from typing import (
- Concatenate,
- Generic,
- ParamSpec,
- TypeVar,
- cast,
-)
-
-from ._base import CRU
-
-_P = ParamSpec("_P")
-_T = TypeVar("_T")
-_O = TypeVar("_O")
-_R = TypeVar("_R")
-
-
-class CruDecorator:
-
- class ConvertResult(Generic[_T, _O]):
- def __init__(
- self,
- converter: Callable[[_T], _O],
- ) -> None:
- self.converter = converter
-
- def __call__(self, origin: Callable[_P, _T]) -> Callable[_P, _O]:
- converter = self.converter
-
- def real_impl(*args: _P.args, **kwargs: _P.kwargs) -> _O:
- return converter(origin(*args, **kwargs))
-
- return real_impl
-
- class ImplementedBy(Generic[_T, _O, _P, _R]):
- def __init__(
- self,
- impl: Callable[Concatenate[_O, _P], _R],
- converter: Callable[[_T], _O],
- ) -> None:
- self.impl = impl
- self.converter = converter
-
- def __call__(
- self, _origin: Callable[[_T], None]
- ) -> Callable[Concatenate[_T, _P], _R]:
- converter = self.converter
- impl = self.impl
-
- def real_impl(_self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R:
- return cast(Callable[Concatenate[_O, _P], _R], impl)(
- converter(_self), *args, **kwargs
- )
-
- return real_impl
-
- @staticmethod
- def create_factory(converter: Callable[[_T], _O]) -> Callable[
- [Callable[Concatenate[_O, _P], _R]],
- CruDecorator.ImplementedBy[_T, _O, _P, _R],
- ]:
- def create(
- m: Callable[Concatenate[_O, _P], _R],
- ) -> CruDecorator.ImplementedBy[_T, _O, _P, _R]:
- return CruDecorator.ImplementedBy(m, converter)
-
- return create
-
- class ImplementedByNoSelf(Generic[_P, _R]):
- def __init__(self, impl: Callable[_P, _R]) -> None:
- self.impl = impl
-
- def __call__(
- self, _origin: Callable[[_T], None]
- ) -> Callable[Concatenate[_T, _P], _R]:
- impl = self.impl
-
- def real_impl(_self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R:
- return cast(Callable[_P, _R], impl)(*args, **kwargs)
-
- return real_impl
-
- @staticmethod
- def create_factory() -> (
- Callable[[Callable[_P, _R]], CruDecorator.ImplementedByNoSelf[_P, _R]]
- ):
- def create(
- m: Callable[_P, _R],
- ) -> CruDecorator.ImplementedByNoSelf[_P, _R]:
- return CruDecorator.ImplementedByNoSelf(m)
-
- return create
-
-
-CRU.add_objects(CruDecorator)
diff --git a/store/works/python/cru/_error.py b/store/works/python/cru/_error.py
deleted file mode 100644
index e53c787..0000000
--- a/store/works/python/cru/_error.py
+++ /dev/null
@@ -1,89 +0,0 @@
-from __future__ import annotations
-
-from typing import NoReturn, cast, overload
-
-
-class CruException(Exception):
- """Base exception class of all exceptions in cru."""
-
- @overload
- def __init__(
- self,
- message: None = None,
- *args,
- user_message: str,
- **kwargs,
- ): ...
-
- @overload
- def __init__(
- self,
- message: str,
- *args,
- user_message: str | None = None,
- **kwargs,
- ): ...
-
- def __init__(
- self,
- message: str | None = None,
- *args,
- user_message: str | None = None,
- **kwargs,
- ):
- if message is None:
- message = user_message
-
- super().__init__(
- message,
- *args,
- **kwargs,
- )
- self._message: str
- self._message = cast(str, message)
- self._user_message = user_message
-
- @property
- def message(self) -> str:
- return self._message
-
- def get_user_message(self) -> str | None:
- return self._user_message
-
- def get_message(self, use_user: bool = True) -> str:
- if use_user and self._user_message is not None:
- return self._user_message
- else:
- return self._message
-
- @property
- def is_internal(self) -> bool:
- return False
-
- @property
- def is_logic_error(self) -> bool:
- return False
-
-
-class CruLogicError(CruException):
- """Raised when a logic error occurs."""
-
- @property
- def is_logic_error(self) -> bool:
- return True
-
-
-class CruInternalError(CruException):
- """Raised when an internal error occurs."""
-
- @property
- def is_internal(self) -> bool:
- return True
-
-
-class CruUnreachableError(CruInternalError):
- """Raised when a code path is unreachable."""
-
-
-def cru_unreachable() -> NoReturn:
- raise CruUnreachableError("Code should not reach here!")
diff --git a/store/works/python/cru/_event.py b/store/works/python/cru/_event.py
deleted file mode 100644
index 51a794c..0000000
--- a/store/works/python/cru/_event.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from __future__ import annotations
-
-from collections.abc import Callable
-from typing import Generic, ParamSpec, TypeVar
-
-from .list import CruList
-
-_P = ParamSpec("_P")
-_R = TypeVar("_R")
-
-
-class CruEventHandlerToken(Generic[_P, _R]):
- def __init__(
- self, event: CruEvent, handler: Callable[_P, _R], once: bool = False
- ) -> None:
- self._event = event
- self._handler = handler
- self._once = once
-
- @property
- def event(self) -> CruEvent:
- return self._event
-
- @property
- def handler(self) -> Callable[_P, _R]:
- return self._handler
-
- @property
- def once(self) -> bool:
- return self._once
-
-
-class CruEvent(Generic[_P, _R]):
- def __init__(self, name: str) -> None:
- self._name = name
- self._tokens: CruList[CruEventHandlerToken] = CruList()
-
- def register(
- self, handler: Callable[_P, _R], once: bool = False
- ) -> CruEventHandlerToken:
- token = CruEventHandlerToken(self, handler, once)
- self._tokens.append(token)
- return token
-
- def unregister(self, *handlers: CruEventHandlerToken | Callable[_P, _R]) -> int:
- old_length = len(self._tokens)
- self._tokens.reset(
- self._tokens.as_cru_iterator().filter(
- (lambda t: t in handlers or t.handler in handlers)
- )
- )
- return old_length - len(self._tokens)
-
- def trigger(self, *args: _P.args, **kwargs: _P.kwargs) -> CruList[_R]:
- results = CruList(
- self._tokens.as_cru_iterator()
- .transform(lambda t: t.handler(*args, **kwargs))
- .to_list()
- )
- self._tokens.reset(self._tokens.as_cru_iterator().filter(lambda t: not t.once))
- return results
diff --git a/store/works/python/cru/_func.py b/store/works/python/cru/_func.py
deleted file mode 100644
index fc57802..0000000
--- a/store/works/python/cru/_func.py
+++ /dev/null
@@ -1,172 +0,0 @@
-from __future__ import annotations
-
-from collections.abc import Callable, Iterable
-from enum import Flag, auto
-from typing import (
- Any,
- Generic,
- Literal,
- ParamSpec,
- TypeAlias,
- TypeVar,
-)
-
-
-from ._base import CRU
-from ._const import CruPlaceholder
-
-_P = ParamSpec("_P")
-_P1 = ParamSpec("_P1")
-_T = TypeVar("_T")
-
-
-class _Dec:
- @staticmethod
- def wrap(
- origin: Callable[_P, Callable[_P1, _T]]
- ) -> Callable[_P, _Wrapper[_P1, _T]]:
- def _wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _Wrapper[_P1, _T]:
- return _Wrapper(origin(*args, **kwargs))
-
- return _wrapped
-
-
-class _RawBase:
- @staticmethod
- def none(*_v, **_kwargs) -> None:
- return None
-
- @staticmethod
- def true(*_v, **_kwargs) -> Literal[True]:
- return True
-
- @staticmethod
- def false(*_v, **_kwargs) -> Literal[False]:
- return False
-
- @staticmethod
- def identity(v: _T) -> _T:
- return v
-
- @staticmethod
- def only_you(v: _T, *_v, **_kwargs) -> _T:
- return v
-
- @staticmethod
- def equal(a: Any, b: Any) -> bool:
- return a == b
-
- @staticmethod
- def not_equal(a: Any, b: Any) -> bool:
- return a != b
-
- @staticmethod
- def not_(v: Any) -> Any:
- return not v
-
-
-class _Wrapper(Generic[_P, _T]):
- def __init__(self, f: Callable[_P, _T]):
- self._f = f
-
- @property
- def me(self) -> Callable[_P, _T]:
- return self._f
-
- def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _T:
- return self._f(*args, **kwargs)
-
- @_Dec.wrap
- def bind(self, *bind_args, **bind_kwargs) -> Callable[..., _T]:
- func = self.me
-
- def bound_func(*args, **kwargs):
- popped = 0
- real_args = []
- for arg in bind_args:
- if CruPlaceholder.check(arg):
- real_args.append(args[popped])
- popped += 1
- else:
- real_args.append(arg)
- real_args.extend(args[popped:])
- return func(*real_args, **(bind_kwargs | kwargs))
-
- return bound_func
-
- class ChainMode(Flag):
- ARGS = auto()
- KWARGS = auto()
- BOTH = ARGS | KWARGS
-
- ArgsChainableCallable: TypeAlias = Callable[..., Iterable[Any]]
- KwargsChainableCallable: TypeAlias = Callable[..., Iterable[tuple[str, Any]]]
- ChainableCallable: TypeAlias = Callable[
- ..., tuple[Iterable[Any], Iterable[tuple[str, Any]]]
- ]
-
- @_Dec.wrap
- def chain_with_args(
- self, funcs: Iterable[ArgsChainableCallable], *bind_args, **bind_kwargs
- ) -> ArgsChainableCallable:
- def chained_func(*args):
- args = self.bind(*bind_args, **bind_kwargs)(*args)
-
- for func in funcs:
- args = _Wrapper(func).bind(*bind_args, **bind_kwargs)(*args)
- return args
-
- return chained_func
-
- @_Dec.wrap
- def chain_with_kwargs(
- self, funcs: Iterable[KwargsChainableCallable], *bind_args, **bind_kwargs
- ) -> KwargsChainableCallable:
- def chained_func(**kwargs):
- kwargs = self.bind(*bind_args, **bind_kwargs)(**kwargs)
- for func in funcs:
- kwargs = _Wrapper(func).bind(func, *bind_args, **bind_kwargs)(**kwargs)
- return kwargs
-
- return chained_func
-
- @_Dec.wrap
- def chain_with_both(
- self, funcs: Iterable[ChainableCallable], *bind_args, **bind_kwargs
- ) -> ChainableCallable:
- def chained_func(*args, **kwargs):
- for func in funcs:
- args, kwargs = _Wrapper(func).bind(func, *bind_args, **bind_kwargs)(
- *args, **kwargs
- )
- return args, kwargs
-
- return chained_func
-
-
-class _Base:
- none = _Wrapper(_RawBase.none)
- true = _Wrapper(_RawBase.true)
- false = _Wrapper(_RawBase.false)
- identity = _Wrapper(_RawBase.identity)
- only_you = _Wrapper(_RawBase.only_you)
- equal = _Wrapper(_RawBase.equal)
- not_equal = _Wrapper(_RawBase.not_equal)
- not_ = _Wrapper(_RawBase.not_)
-
-
-class _Creators:
- @staticmethod
- def make_isinstance_of_types(*types: type) -> Callable:
- return _Wrapper(lambda v: type(v) in types)
-
-
-class CruFunction:
- RawBase: TypeAlias = _RawBase
- Base: TypeAlias = _Base
- Creators: TypeAlias = _Creators
- Wrapper: TypeAlias = _Wrapper
- Decorators: TypeAlias = _Dec
-
-
-CRU.add_objects(CruFunction)
diff --git a/store/works/python/cru/_helper.py b/store/works/python/cru/_helper.py
deleted file mode 100644
index 43baf46..0000000
--- a/store/works/python/cru/_helper.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from collections.abc import Callable
-from typing import Any, Iterable, TypeVar, cast
-
-_T = TypeVar("_T")
-_D = TypeVar("_D")
-
-
-def remove_element(
- iterable: Iterable[_T | None], to_rm: Iterable[Any], des: type[_D] | None = None
-) -> _D:
- to_rm = set(to_rm)
- return cast(Callable[..., _D], des or list)(v for v in iterable if v not in to_rm)
-
-
-def remove_none(iterable: Iterable[_T | None], des: type[_D] | None = None) -> _D:
- return cast(Callable[..., _D], des or list)(v for v in iterable if v is not None)
diff --git a/store/works/python/cru/_iter.py b/store/works/python/cru/_iter.py
deleted file mode 100644
index f9683ca..0000000
--- a/store/works/python/cru/_iter.py
+++ /dev/null
@@ -1,469 +0,0 @@
-from __future__ import annotations
-
-from collections.abc import Iterable, Callable, Generator, Iterator
-from dataclasses import dataclass
-from enum import Enum
-from typing import (
- Concatenate,
- Literal,
- Never,
- Self,
- TypeAlias,
- TypeVar,
- ParamSpec,
- Any,
- Generic,
- cast,
-)
-
-from ._base import CRU
-from ._const import CruNotFound
-from ._error import cru_unreachable
-
-_P = ParamSpec("_P")
-_T = TypeVar("_T")
-_O = TypeVar("_O")
-_V = TypeVar("_V")
-_R = TypeVar("_R")
-
-
-class _Generic:
- class StepActionKind(Enum):
- SKIP = 0
- PUSH = 1
- STOP = 2
- AGGREGATE = 3
-
- @dataclass
- class StepAction(Generic[_V, _R]):
- value: Iterable[Self] | _V | _R | None
- kind: _Generic.StepActionKind
-
- @property
- def push_value(self) -> _V:
- assert self.kind == _Generic.StepActionKind.PUSH
- return cast(_V, self.value)
-
- @property
- def stop_value(self) -> _R:
- assert self.kind == _Generic.StepActionKind.STOP
- return cast(_R, self.value)
-
- @staticmethod
- def skip() -> _Generic.StepAction[_V, _R]:
- return _Generic.StepAction(None, _Generic.StepActionKind.SKIP)
-
- @staticmethod
- def push(value: _V | None) -> _Generic.StepAction[_V, _R]:
- return _Generic.StepAction(value, _Generic.StepActionKind.PUSH)
-
- @staticmethod
- def stop(value: _R | None = None) -> _Generic.StepAction[_V, _R]:
- return _Generic.StepAction(value, _Generic.StepActionKind.STOP)
-
- @staticmethod
- def aggregate(
- *results: _Generic.StepAction[_V, _R],
- ) -> _Generic.StepAction[_V, _R]:
- return _Generic.StepAction(results, _Generic.StepActionKind.AGGREGATE)
-
- @staticmethod
- def push_last(value: _V | None) -> _Generic.StepAction[_V, _R]:
- return _Generic.StepAction.aggregate(
- _Generic.StepAction.push(value), _Generic.StepAction.stop()
- )
-
- def flatten(self) -> Iterable[Self]:
- return _Generic.flatten(
- self,
- is_leave=lambda r: r.kind != _Generic.StepActionKind.AGGREGATE,
- get_children=lambda r: cast(Iterable[Self], r.value),
- )
-
- GeneralStepAction: TypeAlias = StepAction[_V, _R] | _V | _R | None
- IterateOperation: TypeAlias = Callable[[_T, int], GeneralStepAction[_V, _R]]
- IteratePreHook: TypeAlias = Callable[[Iterable[_T]], GeneralStepAction[_V, _R]]
- IteratePostHook: TypeAlias = Callable[[int], GeneralStepAction[_V, _R]]
-
- @staticmethod
- def _is_not_iterable(o: Any) -> bool:
- return not isinstance(o, Iterable)
-
- @staticmethod
- def _return_self(o):
- return o
-
- @staticmethod
- def iterable_flatten(
- maybe_iterable: Iterable[_T] | _T, max_depth: int = -1, *, _depth: int = 0
- ) -> Iterable[Iterable[_T] | _T]:
- if _depth == max_depth or not isinstance(maybe_iterable, Iterable):
- yield maybe_iterable
- return
-
- for child in maybe_iterable:
- yield from _Generic.iterable_flatten(
- child,
- max_depth,
- _depth=_depth + 1,
- )
-
- @staticmethod
- def flatten(
- o: _O,
- max_depth: int = -1,
- /,
- is_leave: CruIterator.ElementPredicate[_O] = _is_not_iterable,
- get_children: CruIterator.ElementTransformer[_O, Iterable[_O]] = _return_self,
- *,
- _depth: int = 0,
- ) -> Iterable[_O]:
- if _depth == max_depth or is_leave(o):
- yield o
- return
- for child in get_children(o):
- yield from _Generic.flatten(
- child,
- max_depth,
- is_leave,
- get_children,
- _depth=_depth + 1,
- )
-
- class Results:
- @staticmethod
- def true(_) -> Literal[True]:
- return True
-
- @staticmethod
- def false(_) -> Literal[False]:
- return False
-
- @staticmethod
- def not_found(_) -> Literal[CruNotFound.VALUE]:
- return CruNotFound.VALUE
-
- @staticmethod
- def _non_result_to_push(value: Any) -> StepAction[_V, _R]:
- return _Generic.StepAction.push(value)
-
- @staticmethod
- def _non_result_to_stop(value: Any) -> StepAction[_V, _R]:
- return _Generic.StepAction.stop(value)
-
- @staticmethod
- def _none_hook(_: Any) -> StepAction[_V, _R]:
- return _Generic.StepAction.skip()
-
- def iterate(
- iterable: Iterable[_T],
- operation: IterateOperation[_T, _V, _R],
- fallback_return: _R,
- pre_iterate: IteratePreHook[_T, _V, _R],
- post_iterate: IteratePostHook[_V, _R],
- convert_value_result: Callable[[_V | _R | None], StepAction[_V, _R]],
- ) -> Generator[_V, None, _R]:
- pre_result = pre_iterate(iterable)
- if not isinstance(pre_result, _Generic.StepAction):
- real_pre_result = convert_value_result(pre_result)
- for r in real_pre_result.flatten():
- if r.kind == _Generic.StepActionKind.STOP:
- return r.stop_value
- elif r.kind == _Generic.StepActionKind.PUSH:
- yield r.push_value
- else:
- assert r.kind == _Generic.StepActionKind.SKIP
-
- for index, element in enumerate(iterable):
- result = operation(element, index)
- if not isinstance(result, _Generic.StepAction):
- real_result = convert_value_result(result)
- for r in real_result.flatten():
- if r.kind == _Generic.StepActionKind.STOP:
- return r.stop_value
- elif r.kind == _Generic.StepActionKind.PUSH:
- yield r.push_value
- else:
- assert r.kind == _Generic.StepActionKind.SKIP
- continue
-
- post_result = post_iterate(index + 1)
- if not isinstance(post_result, _Generic.StepAction):
- real_post_result = convert_value_result(post_result)
- for r in real_post_result.flatten():
- if r.kind == _Generic.StepActionKind.STOP:
- return r.stop_value
- elif r.kind == _Generic.StepActionKind.PUSH:
- yield r.push_value
- else:
- assert r.kind == _Generic.StepActionKind.SKIP
-
- return fallback_return
-
- def create_new(
- iterable: Iterable[_T],
- operation: IterateOperation[_T, _V, _R],
- fallback_return: _R,
- /,
- pre_iterate: IteratePreHook[_T, _V, _R] | None = None,
- post_iterate: IteratePostHook[_V, _R] | None = None,
- ) -> Generator[_V, None, _R]:
- return _Generic.iterate(
- iterable,
- operation,
- fallback_return,
- pre_iterate or _Generic._none_hook,
- post_iterate or _Generic._none_hook,
- _Generic._non_result_to_push,
- )
-
- def get_result(
- iterable: Iterable[_T],
- operation: IterateOperation[_T, _V, _R],
- fallback_return: _R,
- /,
- pre_iterate: IteratePreHook[_T, _V, _R] | None = None,
- post_iterate: IteratePostHook[_V, _R] | None = None,
- ) -> _R:
- try:
- for _ in _Generic.iterate(
- iterable,
- operation,
- fallback_return,
- pre_iterate or _Generic._none_hook,
- post_iterate or _Generic._none_hook,
- _Generic._non_result_to_stop,
- ):
- pass
- except StopIteration as stop:
- return stop.value
- cru_unreachable()
-
-
-class _Helpers:
- @staticmethod
- def auto_count(c: Callable[Concatenate[int, _P], _O]) -> Callable[_P, _O]:
- count = 0
-
- def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _O:
- nonlocal count
- r = c(count, *args, **kwargs)
- count += 1
- return r
-
- return wrapper
-
-
-class _Creators:
- class Raw:
- @staticmethod
- def empty() -> Iterator[Never]:
- return iter([])
-
- @staticmethod
- def range(*args) -> Iterator[int]:
- return iter(range(*args))
-
- @staticmethod
- def unite(*args: _T) -> Iterator[_T]:
- return iter(args)
-
- @staticmethod
- def _concat(*iterables: Iterable[_T]) -> Iterable[_T]:
- for iterable in iterables:
- yield from iterable
-
- @staticmethod
- def concat(*iterables: Iterable[_T]) -> Iterator[_T]:
- return iter(_Creators.Raw._concat(*iterables))
-
- @staticmethod
- def _wrap(f: Callable[_P, Iterable[_O]]) -> Callable[_P, CruIterator[_O]]:
- def _wrapped(*args: _P.args, **kwargs: _P.kwargs) -> CruIterator[_O]:
- return CruIterator(f(*args, **kwargs))
-
- return _wrapped
-
- empty = _wrap(Raw.empty)
- range = _wrap(Raw.range)
- unite = _wrap(Raw.unite)
- concat = _wrap(Raw.concat)
-
-
-class CruIterator(Generic[_T]):
- ElementOperation: TypeAlias = Callable[[_V], Any]
- ElementPredicate: TypeAlias = Callable[[_V], bool]
- AnyElementPredicate: TypeAlias = ElementPredicate[Any]
- ElementTransformer: TypeAlias = Callable[[_V], _O]
- SelfElementTransformer: TypeAlias = ElementTransformer[_V, _V]
- AnyElementTransformer: TypeAlias = ElementTransformer[Any, Any]
-
- Creators: TypeAlias = _Creators
- Helpers: TypeAlias = _Helpers
-
- def __init__(self, iterable: Iterable[_T]) -> None:
- self._iterator = iter(iterable)
-
- def __iter__(self) -> Iterator[_T]:
- return self._iterator
-
- def create_new_me(self, iterable: Iterable[_O]) -> CruIterator[_O]:
- return type(self)(iterable) # type: ignore
-
- @staticmethod
- def _wrap(
- f: Callable[Concatenate[CruIterator[_T], _P], Iterable[_O]],
- ) -> Callable[Concatenate[CruIterator[_T], _P], CruIterator[_O]]:
- def _wrapped(
- self: CruIterator[_T], *args: _P.args, **kwargs: _P.kwargs
- ) -> CruIterator[_O]:
- return self.create_new_me(f(self, *args, **kwargs))
-
- return _wrapped
-
- @_wrap
- def replace_me(self, iterable: Iterable[_O]) -> Iterable[_O]:
- return iterable
-
- def replace_me_with_empty(self) -> CruIterator[Never]:
- return self.create_new_me(_Creators.Raw.empty())
-
- def replace_me_with_range(self, *args) -> CruIterator[int]:
- return self.create_new_me(_Creators.Raw.range(*args))
-
- def replace_me_with_unite(self, *args: _O) -> CruIterator[_O]:
- return self.create_new_me(_Creators.Raw.unite(*args))
-
- def replace_me_with_concat(self, *iterables: Iterable[_T]) -> CruIterator[_T]:
- return self.create_new_me(_Creators.Raw.concat(*iterables))
-
- def to_set(self) -> set[_T]:
- return set(self)
-
- def to_list(self) -> list[_T]:
- return list(self)
-
- def all(self, predicate: ElementPredicate[_T]) -> bool:
- for value in self:
- if not predicate(value):
- return False
- return True
-
- def any(self, predicate: ElementPredicate[_T]) -> bool:
- for value in self:
- if predicate(value):
- return True
- return False
-
- def foreach(self, operation: ElementOperation[_T]) -> None:
- for value in self:
- operation(value)
-
- @_wrap
- def transform(self, transformer: ElementTransformer[_T, _O]) -> Iterable[_O]:
- for value in self:
- yield transformer(value)
-
- map = transform
-
- @_wrap
- def filter(self, predicate: ElementPredicate[_T]) -> Iterable[_T]:
- for value in self:
- if predicate(value):
- yield value
-
- @_wrap
- def continue_if(self, predicate: ElementPredicate[_T]) -> Iterable[_T]:
- for value in self:
- yield value
- if not predicate(value):
- break
-
- def first_n(self, max_count: int) -> CruIterator[_T]:
- if max_count < 0:
- raise ValueError("max_count must be 0 or positive.")
- if max_count == 0:
- return self.replace_me_with_empty() # type: ignore
- return self.continue_if(_Helpers.auto_count(lambda i, _: i < max_count - 1))
-
- def drop_n(self, n: int) -> CruIterator[_T]:
- if n < 0:
- raise ValueError("n must be 0 or positive.")
- if n == 0:
- return self
- return self.filter(_Helpers.auto_count(lambda i, _: i < n))
-
- def single_or(
- self, fallback: _O | CruNotFound = CruNotFound.VALUE
- ) -> _T | _O | CruNotFound:
- first_2 = self.first_n(2)
- has_value = False
- for element in first_2:
- if has_value:
- raise ValueError("More than one value found.")
- has_value = True
- value = element
- if has_value:
- return value
- else:
- return fallback
-
- def first_or(
- self, fallback: _O | CruNotFound = CruNotFound.VALUE
- ) -> _T | _O | CruNotFound:
- return self.first_n(1).single_or(fallback)
-
- @_wrap
- def flatten(self, max_depth: int = -1) -> Iterable[Any]:
- return _Generic.iterable_flatten(self, max_depth)
-
- def select_by_indices(self, indices: Iterable[int]) -> CruIterator[_T]:
- index_set = set(indices)
- max_index = max(index_set)
- return self.first_n(max_index + 1).filter(
- _Helpers.auto_count(lambda i, _: i in index_set)
- )
-
- def remove_values(self, values: Iterable[Any]) -> CruIterator[_T]:
- value_set = set(values)
- return self.filter(lambda v: v not in value_set)
-
- def replace_values(
- self, old_values: Iterable[Any], new_value: _O
- ) -> Iterable[_T | _O]:
- value_set = set(old_values)
- return self.transform(lambda v: new_value if v in value_set else v)
-
- def group_by(self, key_getter: Callable[[_T], _O]) -> dict[_O, list[_T]]:
- result: dict[_O, list[_T]] = {}
-
- for item in self:
- key = key_getter(item)
- if key not in result:
- result[key] = []
- result[key].append(item)
-
- return result
-
- def join_str(self: CruIterator[str], separator: str) -> str:
- return separator.join(self)
-
-
-class CruIterMixin(Generic[_T]):
- def cru_iter(self: Iterable[_T]) -> CruIterator[_T]:
- return CruIterator(self)
-
-
-class CruIterList(list[_T], CruIterMixin[_T]):
- pass
-
-
-class CruIterable:
- Generic: TypeAlias = _Generic
- Iterator: TypeAlias = CruIterator[_T]
- Helpers: TypeAlias = _Helpers
- Mixin: TypeAlias = CruIterMixin[_T]
- IterList: TypeAlias = CruIterList[_T]
-
-
-CRU.add_objects(CruIterable, CruIterator)
diff --git a/store/works/python/cru/_type.py b/store/works/python/cru/_type.py
deleted file mode 100644
index 1f81da3..0000000
--- a/store/works/python/cru/_type.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from collections.abc import Iterable
-from typing import Any
-
-from ._error import CruException, CruLogicError
-from ._iter import CruIterator
-
-
-class CruTypeCheckError(CruException):
- pass
-
-
-DEFAULT_NONE_ERR_MSG = "None is not allowed here."
-DEFAULT_TYPE_ERR_MSG = "Object of this type is not allowed here."
-
-
-class CruTypeSet(set[type]):
- def __init__(self, *types: type):
- type_set = CruIterator(types).filter(lambda t: t is not None).to_set()
- if not CruIterator(type_set).all(lambda t: isinstance(t, type)):
- raise CruLogicError("TypeSet can only contain type.")
- super().__init__(type_set)
-
- def check_value(
- self,
- value: Any,
- /,
- allow_none: bool = False,
- empty_allow_all: bool = True,
- ) -> None:
- if value is None:
- if allow_none:
- return
- else:
- raise CruTypeCheckError(DEFAULT_NONE_ERR_MSG)
- if len(self) == 0 and empty_allow_all:
- return
- if not CruIterator(self).any(lambda t: isinstance(value, t)):
- raise CruTypeCheckError(DEFAULT_TYPE_ERR_MSG)
-
- def check_value_list(
- self,
- values: Iterable[Any],
- /,
- allow_none: bool = False,
- empty_allow_all: bool = True,
- ) -> None:
- for value in values:
- self.check_value(
- value,
- allow_none,
- empty_allow_all,
- )
diff --git a/store/works/python/cru/attr.py b/store/works/python/cru/attr.py
deleted file mode 100644
index d4cc86a..0000000
--- a/store/works/python/cru/attr.py
+++ /dev/null
@@ -1,364 +0,0 @@
-from __future__ import annotations
-
-import copy
-from collections.abc import Callable, Iterable
-from dataclasses import dataclass, field
-from typing import Any
-
-from .list import CruUniqueKeyList
-from ._type import CruTypeSet
-from ._const import CruNotFound, CruUseDefault, CruDontChange
-from ._iter import CruIterator
-
-
-@dataclass
-class CruAttr:
-
- name: str
- value: Any
- description: str | None
-
- @staticmethod
- def make(
- name: str, value: Any = CruUseDefault.VALUE, description: str | None = None
- ) -> CruAttr:
- return CruAttr(name, value, description)
-
-
-CruAttrDefaultFactory = Callable[["CruAttrDef"], Any]
-CruAttrTransformer = Callable[[Any, "CruAttrDef"], Any]
-CruAttrValidator = Callable[[Any, "CruAttrDef"], None]
-
-
-@dataclass
-class CruAttrDef:
- name: str
- description: str
- default_factory: CruAttrDefaultFactory
- transformer: CruAttrTransformer
- validator: CruAttrValidator
-
- def __init__(
- self,
- name: str,
- description: str,
- default_factory: CruAttrDefaultFactory,
- transformer: CruAttrTransformer,
- validator: CruAttrValidator,
- ) -> None:
- self.name = name
- self.description = description
- self.default_factory = default_factory
- self.transformer = transformer
- self.validator = validator
-
- def transform(self, value: Any) -> Any:
- if self.transformer is not None:
- return self.transformer(value, self)
- return value
-
- def validate(self, value: Any, /, force_allow_none: bool = False) -> None:
- if force_allow_none is value is None:
- return
- if self.validator is not None:
- self.validator(value, self)
-
- def transform_and_validate(
- self, value: Any, /, force_allow_none: bool = False
- ) -> Any:
- value = self.transform(value)
- self.validate(value, force_allow_none)
- return value
-
- def make_default_value(self) -> Any:
- return self.transform_and_validate(self.default_factory(self))
-
- def adopt(self, attr: CruAttr) -> CruAttr:
- attr = copy.deepcopy(attr)
-
- if attr.name is None:
- attr.name = self.name
- elif attr.name != self.name:
- raise ValueError(f"Attr name is not match: {attr.name} != {self.name}")
-
- if attr.value is CruUseDefault.VALUE:
- attr.value = self.make_default_value()
- else:
- attr.value = self.transform_and_validate(attr.value)
-
- if attr.description is None:
- attr.description = self.description
-
- return attr
-
- def make(
- self, value: Any = CruUseDefault.VALUE, description: None | str = None
- ) -> CruAttr:
- value = self.make_default_value() if value is CruUseDefault.VALUE else value
- value = self.transform_and_validate(value)
- return CruAttr(
- self.name,
- value,
- description if description is not None else self.description,
- )
-
-
-@dataclass
-class CruAttrDefBuilder:
-
- name: str
- description: str
- types: list[type] | None = field(default=None)
- allow_none: bool = field(default=False)
- default: Any = field(default=CruUseDefault.VALUE)
- default_factory: CruAttrDefaultFactory | None = field(default=None)
- auto_list: bool = field(default=False)
- transformers: list[CruAttrTransformer] = field(default_factory=list)
- validators: list[CruAttrValidator] = field(default_factory=list)
- override_transformer: CruAttrTransformer | None = field(default=None)
- override_validator: CruAttrValidator | None = field(default=None)
-
- build_hook: Callable[[CruAttrDef], None] | None = field(default=None)
-
- def __init__(self, name: str, description: str) -> None:
- super().__init__()
- self.name = name
- self.description = description
-
- def auto_adjust_default(self) -> None:
- if self.default is not CruUseDefault.VALUE and self.default is not None:
- return
- if self.allow_none and self.default is CruUseDefault.VALUE:
- self.default = None
- if not self.allow_none and self.default is None:
- self.default = CruUseDefault.VALUE
- if self.auto_list and not self.allow_none:
- self.default = []
-
- def with_name(self, name: str | CruDontChange) -> CruAttrDefBuilder:
- if name is not CruDontChange.VALUE:
- self.name = name
- return self
-
- def with_description(
- self, default_description: str | CruDontChange
- ) -> CruAttrDefBuilder:
- if default_description is not CruDontChange.VALUE:
- self.description = default_description
- return self
-
- def with_default(self, default: Any) -> CruAttrDefBuilder:
- if default is not CruDontChange.VALUE:
- self.default = default
- return self
-
- def with_default_factory(
- self,
- default_factory: CruAttrDefaultFactory | CruDontChange,
- ) -> CruAttrDefBuilder:
- if default_factory is not CruDontChange.VALUE:
- self.default_factory = default_factory
- return self
-
- def with_types(
- self,
- types: Iterable[type] | None | CruDontChange,
- ) -> CruAttrDefBuilder:
- if types is not CruDontChange.VALUE:
- self.types = None if types is None else list(types)
- return self
-
- def with_allow_none(self, allow_none: bool | CruDontChange) -> CruAttrDefBuilder:
- if allow_none is not CruDontChange.VALUE:
- self.allow_none = allow_none
- return self
-
- def with_auto_list(
- self, auto_list: bool | CruDontChange = True
- ) -> CruAttrDefBuilder:
- if auto_list is not CruDontChange.VALUE:
- self.auto_list = auto_list
- return self
-
- def with_constraint(
- self,
- /,
- allow_none: bool | CruDontChange = CruDontChange.VALUE,
- types: Iterable[type] | None | CruDontChange = CruDontChange.VALUE,
- default: Any = CruDontChange.VALUE,
- default_factory: CruAttrDefaultFactory | CruDontChange = CruDontChange.VALUE,
- auto_list: bool | CruDontChange = CruDontChange.VALUE,
- ) -> CruAttrDefBuilder:
- return (
- self.with_allow_none(allow_none)
- .with_types(types)
- .with_default(default)
- .with_default_factory(default_factory)
- .with_auto_list(auto_list)
- )
-
- def add_transformer(self, transformer: CruAttrTransformer) -> CruAttrDefBuilder:
- self.transformers.append(transformer)
- return self
-
- def clear_transformers(self) -> CruAttrDefBuilder:
- self.transformers.clear()
- return self
-
- def add_validator(self, validator: CruAttrValidator) -> CruAttrDefBuilder:
- self.validators.append(validator)
- return self
-
- def clear_validators(self) -> CruAttrDefBuilder:
- self.validators.clear()
- return self
-
- def with_override_transformer(
- self, override_transformer: CruAttrTransformer | None | CruDontChange
- ) -> CruAttrDefBuilder:
- if override_transformer is not CruDontChange.VALUE:
- self.override_transformer = override_transformer
- return self
-
- def with_override_validator(
- self, override_validator: CruAttrValidator | None | CruDontChange
- ) -> CruAttrDefBuilder:
- if override_validator is not CruDontChange.VALUE:
- self.override_validator = override_validator
- return self
-
- def is_valid(self) -> tuple[bool, str]:
- if not isinstance(self.name, str):
- return False, "Name must be a string!"
- if not isinstance(self.description, str):
- return False, "Default description must be a string!"
- if (
- not self.allow_none
- and self.default is None
- and self.default_factory is None
- ):
- return False, "Default must be set if allow_none is False!"
- return True, ""
-
- @staticmethod
- def _build(
- builder: CruAttrDefBuilder, auto_adjust_default: bool = True
- ) -> CruAttrDef:
- if auto_adjust_default:
- builder.auto_adjust_default()
-
- valid, err = builder.is_valid()
- if not valid:
- raise ValueError(err)
-
- def composed_transformer(value: Any, attr_def: CruAttrDef) -> Any:
- def transform_value(single_value: Any) -> Any:
- for transformer in builder.transformers:
- single_value = transformer(single_value, attr_def)
- return single_value
-
- if builder.auto_list:
- if not isinstance(value, list):
- value = [value]
- value = CruIterator(value).transform(transform_value).to_list()
-
- else:
- value = transform_value(value)
- return value
-
- type_set = None if builder.types is None else CruTypeSet(*builder.types)
-
- def composed_validator(value: Any, attr_def: CruAttrDef):
- def validate_value(single_value: Any) -> None:
- if type_set is not None:
- type_set.check_value(single_value, allow_none=builder.allow_none)
- for validator in builder.validators:
- validator(single_value, attr_def)
-
- if builder.auto_list:
- CruIterator(value).foreach(validate_value)
- else:
- validate_value(value)
-
- real_transformer = builder.override_transformer or composed_transformer
- real_validator = builder.override_validator or composed_validator
-
- default_factory = builder.default_factory
- if default_factory is None:
-
- def default_factory(_d):
- return copy.deepcopy(builder.default)
-
- d = CruAttrDef(
- builder.name,
- builder.description,
- default_factory,
- real_transformer,
- real_validator,
- )
- if builder.build_hook:
- builder.build_hook(d)
- return d
-
- def build(self, auto_adjust_default=True) -> CruAttrDef:
- c = copy.deepcopy(self)
- self.build_hook = None
- return CruAttrDefBuilder._build(c, auto_adjust_default)
-
-
-class CruAttrDefRegistry(CruUniqueKeyList[CruAttrDef, str]):
-
- def __init__(self) -> None:
- super().__init__(lambda d: d.name)
-
- def make_builder(self, name: str, default_description: str) -> CruAttrDefBuilder:
- b = CruAttrDefBuilder(name, default_description)
- b.build_hook = lambda a: self.add(a)
- return b
-
- def adopt(self, attr: CruAttr) -> CruAttr:
- d = self.get(attr.name)
- return d.adopt(attr)
-
-
-class CruAttrTable(CruUniqueKeyList[CruAttr, str]):
- def __init__(self, registry: CruAttrDefRegistry) -> None:
- self._registry: CruAttrDefRegistry = registry
- super().__init__(lambda a: a.name, before_add=registry.adopt)
-
- @property
- def registry(self) -> CruAttrDefRegistry:
- return self._registry
-
- def get_value_or(self, name: str, fallback: Any = CruNotFound.VALUE) -> Any:
- a = self.get_or(name, CruNotFound.VALUE)
- if a is CruNotFound.VALUE:
- return fallback
- return a.value
-
- def get_value(self, name: str) -> Any:
- a = self.get(name)
- return a.value
-
- def make_attr(
- self,
- name: str,
- value: Any = CruUseDefault.VALUE,
- /,
- description: str | None = None,
- ) -> CruAttr:
- d = self._registry.get(name)
- return d.make(value, description or d.description)
-
- def add_value(
- self,
- name: str,
- value: Any = CruUseDefault.VALUE,
- /,
- description: str | None = None,
- *,
- replace: bool = False,
- ) -> CruAttr:
- attr = self.make_attr(name, value, description)
- self.add(attr, replace)
- return attr
diff --git a/store/works/python/cru/config.py b/store/works/python/cru/config.py
deleted file mode 100644
index 0f6f0d0..0000000
--- a/store/works/python/cru/config.py
+++ /dev/null
@@ -1,196 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, TypeVar, Generic
-
-from ._error import CruException
-from .list import CruUniqueKeyList
-from .value import (
- INTEGER_VALUE_TYPE,
- TEXT_VALUE_TYPE,
- CruValueTypeError,
- ValueGeneratorBase,
- ValueType,
-)
-
-_T = TypeVar("_T")
-
-
-class CruConfigError(CruException):
- def __init__(self, message: str, item: ConfigItem, *args, **kwargs):
- super().__init__(message, *args, **kwargs)
- self._item = item
-
- @property
- def item(self) -> ConfigItem:
- return self._item
-
-
-class ConfigItem(Generic[_T]):
- def __init__(
- self,
- name: str,
- description: str,
- value_type: ValueType[_T],
- value: _T | None = None,
- /,
- default: ValueGeneratorBase[_T] | _T | None = None,
- ) -> None:
- self._name = name
- self._description = description
- self._value_type = value_type
- self._value = value
- self._default = default
-
- @property
- def name(self) -> str:
- return self._name
-
- @property
- def description(self) -> str:
- return self._description
-
- @property
- def value_type(self) -> ValueType[_T]:
- return self._value_type
-
- @property
- def is_set(self) -> bool:
- return self._value is not None
-
- @property
- def value(self) -> _T:
- if self._value is None:
- raise CruConfigError(
- "Config value is not set.",
- self,
- user_message=f"Config item {self.name} is not set.",
- )
- return self._value
-
- @property
- def value_str(self) -> str:
- return self.value_type.convert_value_to_str(self.value)
-
- def set_value(self, v: _T | str, allow_convert_from_str=False):
- if allow_convert_from_str:
- self._value = self.value_type.check_value_or_try_convert_from_str(v)
- else:
- self._value = self.value_type.check_value(v)
-
- def reset(self):
- self._value = None
-
- @property
- def default(self) -> ValueGeneratorBase[_T] | _T | None:
- return self._default
-
- @property
- def can_generate_default(self) -> bool:
- return self.default is not None
-
- def generate_default_value(self) -> _T:
- if self.default is None:
- raise CruConfigError(
- "Config item does not support default value generation.", self
- )
- elif isinstance(self.default, ValueGeneratorBase):
- v = self.default.generate()
- else:
- v = self.default
- try:
- self.value_type.check_value(v)
- return v
- except CruValueTypeError as e:
- raise CruConfigError(
- "Config value generator returns an invalid value.", self
- ) from e
-
- def copy(self) -> "ConfigItem":
- return ConfigItem(
- self.name,
- self.description,
- self.value_type,
- self.value,
- self.default,
- )
-
- @property
- def description_str(self) -> str:
- return f"{self.name} ({self.value_type.name}): {self.description}"
-
-
-class Configuration(CruUniqueKeyList[ConfigItem[Any], str]):
- def __init__(self):
- super().__init__(lambda c: c.name)
-
- def get_set_items(self) -> list[ConfigItem[Any]]:
- return [item for item in self if item.is_set]
-
- def get_unset_items(self) -> list[ConfigItem[Any]]:
- return [item for item in self if not item.is_set]
-
- @property
- def all_set(self) -> bool:
- return len(self.get_unset_items()) == 0
-
- @property
- def all_not_set(self) -> bool:
- return len(self.get_set_items()) == 0
-
- def add_text_config(
- self,
- name: str,
- description: str,
- value: str | None = None,
- default: ValueGeneratorBase[str] | str | None = None,
- ) -> ConfigItem[str]:
- item = ConfigItem(name, description, TEXT_VALUE_TYPE, value, default)
- self.add(item)
- return item
-
- def add_int_config(
- self,
- name: str,
- description: str,
- value: int | None = None,
- default: ValueGeneratorBase[int] | int | None = None,
- ) -> ConfigItem[int]:
- item = ConfigItem(name, description, INTEGER_VALUE_TYPE, value, default)
- self.add(item)
- return item
-
- def set_config_item(
- self,
- name: str,
- value: Any | str,
- allow_convert_from_str=True,
- ) -> None:
- item = self.get(name)
- item.set_value(
- value,
- allow_convert_from_str=allow_convert_from_str,
- )
-
- def reset_all(self) -> None:
- for item in self:
- item.reset()
-
- def to_dict(self) -> dict[str, Any]:
- return {item.name: item.value for item in self}
-
- def to_str_dict(self) -> dict[str, str]:
- return {
- item.name: item.value_type.convert_value_to_str(item.value) for item in self
- }
-
- def set_value_dict(
- self,
- value_dict: dict[str, Any],
- allow_convert_from_str: bool = False,
- ) -> None:
- for name, value in value_dict.items():
- item = self.get(name)
- item.set_value(
- value,
- allow_convert_from_str=allow_convert_from_str,
- )
diff --git a/store/works/python/cru/list.py b/store/works/python/cru/list.py
deleted file mode 100644
index 216a561..0000000
--- a/store/works/python/cru/list.py
+++ /dev/null
@@ -1,160 +0,0 @@
-from __future__ import annotations
-
-from collections.abc import Callable, Iterator
-from typing import Any, Generic, Iterable, TypeAlias, TypeVar, overload
-
-from ._error import CruInternalError
-from ._iter import CruIterator
-from ._const import CruNotFound
-
-_T = TypeVar("_T")
-_O = TypeVar("_O")
-
-
-class CruListEdit(CruIterator[_T]):
- def __init__(self, iterable: Iterable[_T], _list: CruList[Any]) -> None:
- super().__init__(iterable)
- self._list = _list
-
- def create_me(self, iterable: Iterable[_O]) -> CruListEdit[_O]:
- return CruListEdit(iterable, self._list)
-
- @property
- def list(self) -> CruList[Any]:
- return self._list
-
- def done(self) -> CruList[Any]:
- self._list.reset(self)
- return self._list
-
-
-class CruList(list[_T]):
- def reset(self, new_values: Iterable[_T]):
- if self is new_values:
- new_values = list(new_values)
- self.clear()
- self.extend(new_values)
- return self
-
- def as_cru_iterator(self) -> CruIterator[_T]:
- return CruIterator(self)
-
- @staticmethod
- def make(maybe_list: Iterable[_T] | _T | None) -> CruList[_T]:
- if maybe_list is None:
- return CruList()
- if isinstance(maybe_list, Iterable):
- return CruList(maybe_list)
- return CruList([maybe_list])
-
-
-_K = TypeVar("_K")
-
-_KeyGetter: TypeAlias = Callable[[_T], _K]
-
-
-class CruUniqueKeyList(Generic[_T, _K]):
- def __init__(
- self,
- key_getter: _KeyGetter[_T, _K],
- *,
- before_add: Callable[[_T], _T] | None = None,
- ):
- super().__init__()
- self._key_getter = key_getter
- self._before_add = before_add
- self._list: CruList[_T] = CruList()
-
- @property
- def key_getter(self) -> _KeyGetter[_T, _K]:
- return self._key_getter
-
- @property
- def internal_list(self) -> CruList[_T]:
- return self._list
-
- def validate_self(self):
- keys = self._list.transform(self._key_getter)
- if len(keys) != len(set(keys)):
- raise CruInternalError("Duplicate keys!")
-
- @overload
- def get_or(
- self, key: _K, fallback: CruNotFound = CruNotFound.VALUE
- ) -> _T | CruNotFound: ...
-
- @overload
- def get_or(self, key: _K, fallback: _O) -> _T | _O: ...
-
- def get_or(
- self, key: _K, fallback: _O | CruNotFound = CruNotFound.VALUE
- ) -> _T | _O | CruNotFound:
- return (
- self._list.as_cru_iterator()
- .filter(lambda v: key == self._key_getter(v))
- .first_or(fallback)
- )
-
- def get(self, key: _K) -> _T:
- value = self.get_or(key)
- if value is CruNotFound.VALUE:
- raise KeyError(f"Key {key} not found!")
- return value # type: ignore
-
- @property
- def keys(self) -> Iterable[_K]:
- return self._list.as_cru_iterator().map(self._key_getter)
-
- def has_key(self, key: _K) -> bool:
- return self.get_or(key) != CruNotFound.VALUE
-
- def try_remove(self, key: _K) -> bool:
- value = self.get_or(key)
- if value is CruNotFound.VALUE:
- return False
- self._list.remove(value)
- return True
-
- def remove(self, key: _K, allow_absence: bool = False) -> None:
- if not self.try_remove(key) and not allow_absence:
- raise KeyError(f"Key {key} not found!")
-
- def add(self, value: _T, /, replace: bool = False) -> None:
- v = self.get_or(self._key_getter(value))
- if v is not CruNotFound.VALUE:
- if not replace:
- raise KeyError(f"Key {self._key_getter(v)} already exists!")
- self._list.remove(v)
- if self._before_add is not None:
- value = self._before_add(value)
- self._list.append(value)
-
- def set(self, value: _T) -> None:
- self.add(value, True)
-
- def extend(self, iterable: Iterable[_T], /, replace: bool = False) -> None:
- values = list(iterable)
- to_remove = []
- for value in values:
- v = self.get_or(self._key_getter(value))
- if v is not CruNotFound.VALUE:
- if not replace:
- raise KeyError(f"Key {self._key_getter(v)} already exists!")
- to_remove.append(v)
- for value in to_remove:
- self._list.remove(value)
- if self._before_add is not None:
- values = [self._before_add(value) for value in values]
- self._list.extend(values)
-
- def clear(self) -> None:
- self._list.reset([])
-
- def __iter__(self) -> Iterator[_T]:
- return iter(self._list)
-
- def __len__(self) -> int:
- return len(self._list)
-
- def cru_iter(self) -> CruIterator[_T]:
- return CruIterator(self._list)
diff --git a/store/works/python/cru/parsing.py b/store/works/python/cru/parsing.py
deleted file mode 100644
index 0e9239d..0000000
--- a/store/works/python/cru/parsing.py
+++ /dev/null
@@ -1,290 +0,0 @@
-from __future__ import annotations
-
-from abc import ABCMeta, abstractmethod
-from dataclasses import dataclass
-from enum import Enum
-from typing import NamedTuple, TypeAlias, TypeVar, Generic, NoReturn, Callable
-
-from ._error import CruException
-from ._iter import CruIterable
-
-_T = TypeVar("_T")
-
-
-class StrParseStream:
- class MemStackEntry(NamedTuple):
- pos: int
- lineno: int
-
- class MemStackPopStr(NamedTuple):
- text: str
- lineno: int
-
- def __init__(self, text: str) -> None:
- self._text = text
- self._pos = 0
- self._lineno = 1
- self._length = len(self._text)
- self._valid_pos_range = range(0, self.length + 1)
- self._valid_offset_range = range(-self.length, self.length + 1)
- self._mem_stack: CruIterable.IterList[StrParseStream.MemStackEntry] = (
- CruIterable.IterList()
- )
-
- @property
- def text(self) -> str:
- return self._text
-
- @property
- def length(self) -> int:
- return self._length
-
- @property
- def valid_pos_range(self) -> range:
- return self._valid_pos_range
-
- @property
- def valid_offset_range(self) -> range:
- return self._valid_offset_range
-
- @property
- def pos(self) -> int:
- return self._pos
-
- @property
- def lineno(self) -> int:
- return self._lineno
-
- @property
- def eof(self) -> bool:
- return self._pos == self.length
-
- def peek(self, length: int) -> str:
- real_length = min(length, self.length - self._pos)
- new_position = self._pos + real_length
- text = self._text[self._pos : new_position]
- return text
-
- def read(self, length: int) -> str:
- text = self.peek(length)
- self._pos += len(text)
- self._lineno += text.count("\n")
- return text
-
- def skip(self, length: int) -> None:
- self.read(length)
-
- def peek_str(self, text: str) -> bool:
- if self.pos + len(text) > self.length:
- return False
- for offset in range(len(text)):
- if self._text[self.pos + offset] != text[offset]:
- return False
- return True
-
- def read_str(self, text: str) -> bool:
- if not self.peek_str(text):
- return False
- self._pos += len(text)
- self._lineno += text.count("\n")
- return True
-
- @property
- def mem_stack(self) -> CruIterable.IterList[MemStackEntry]:
- return self._mem_stack
-
- def push_mem(self) -> None:
- self.mem_stack.append(self.MemStackEntry(self.pos, self.lineno))
-
- def pop_mem(self) -> MemStackEntry:
- return self.mem_stack.pop()
-
- def pop_mem_str(self, strip_end: int = 0) -> MemStackPopStr:
- old = self.pop_mem()
- assert self.pos >= old.pos
- return self.MemStackPopStr(
- self._text[old.pos : self.pos - strip_end], old.lineno
- )
-
-
-class ParseError(CruException, Generic[_T]):
- def __init__(
- self,
- message,
- parser: Parser[_T],
- text: str,
- line_number: int | None = None,
- *args,
- **kwargs,
- ):
- super().__init__(message, *args, **kwargs)
- self._parser = parser
- self._text = text
- self._line_number = line_number
-
- @property
- def parser(self) -> Parser[_T]:
- return self._parser
-
- @property
- def text(self) -> str:
- return self._text
-
- @property
- def line_number(self) -> int | None:
- return self._line_number
-
-
-class Parser(Generic[_T], metaclass=ABCMeta):
- def __init__(self, name: str) -> None:
- self._name = name
-
- @property
- def name(self) -> str:
- return self._name
-
- @abstractmethod
- def parse(self, s: str) -> _T:
- raise NotImplementedError()
-
- def raise_parse_exception(
- self, text: str, line_number: int | None = None
- ) -> NoReturn:
- a = line_number and f" at line {line_number}" or ""
- raise ParseError(f"Parser {self.name} failed{a}.", self, text, line_number)
-
-
-class _SimpleLineVarParserEntry(NamedTuple):
- key: str
- value: str
- line_number: int | None = None
-
-
-class _SimpleLineVarParserResult(CruIterable.IterList[_SimpleLineVarParserEntry]):
- pass
-
-
-class SimpleLineVarParser(Parser[_SimpleLineVarParserResult]):
- """
- The parsing result is a list of tuples (key, value, line number).
- """
-
- Entry: TypeAlias = _SimpleLineVarParserEntry
- Result: TypeAlias = _SimpleLineVarParserResult
-
- def __init__(self) -> None:
- super().__init__(type(self).__name__)
-
- def _parse(self, text: str, callback: Callable[[Entry], None]) -> None:
- for ln, line in enumerate(text.splitlines()):
- line_number = ln + 1
- # check if it's a comment
- if line.strip().startswith("#"):
- continue
- # check if there is a '='
- if line.find("=") == -1:
- self.raise_parse_exception("There is even no '='!", line_number)
- # split at first '='
- key, value = line.split("=", 1)
- key = key.strip()
- value = value.strip()
- callback(_SimpleLineVarParserEntry(key, value, line_number))
-
- def parse(self, text: str) -> Result:
- result = _SimpleLineVarParserResult()
- self._parse(text, lambda item: result.append(item))
- return result
-
-
-class _StrWrapperVarParserTokenKind(Enum):
- TEXT = "TEXT"
- VAR = "VAR"
-
-
-@dataclass
-class _StrWrapperVarParserToken:
- kind: _StrWrapperVarParserTokenKind
- value: str
- line_number: int
-
- @property
- def is_text(self) -> bool:
- return self.kind is _StrWrapperVarParserTokenKind.TEXT
-
- @property
- def is_var(self) -> bool:
- return self.kind is _StrWrapperVarParserTokenKind.VAR
-
- @staticmethod
- def from_mem_str(
- kind: _StrWrapperVarParserTokenKind, mem_str: StrParseStream.MemStackPopStr
- ) -> _StrWrapperVarParserToken:
- return _StrWrapperVarParserToken(kind, mem_str.text, mem_str.lineno)
-
- def __repr__(self) -> str:
- return f"VAR: {self.value}" if self.is_var else "TEXT: ..."
-
-
-class _StrWrapperVarParserResult(CruIterable.IterList[_StrWrapperVarParserToken]):
- pass
-
-
-class StrWrapperVarParser(Parser[_StrWrapperVarParserResult]):
- TokenKind: TypeAlias = _StrWrapperVarParserTokenKind
- Token: TypeAlias = _StrWrapperVarParserToken
- Result: TypeAlias = _StrWrapperVarParserResult
-
- def __init__(self, wrapper: str):
- super().__init__(f"StrWrapperVarParser({wrapper})")
- self._wrapper = wrapper
-
- @property
- def wrapper(self) -> str:
- return self._wrapper
-
- def parse(self, text: str) -> Result:
- result = self.Result()
-
- class _State(Enum):
- TEXT = "TEXT"
- VAR = "VAR"
-
- state = _State.TEXT
- stream = StrParseStream(text)
- stream.push_mem()
-
- while True:
- if stream.eof:
- break
-
- if stream.read_str(self.wrapper):
- if state is _State.TEXT:
- result.append(
- self.Token.from_mem_str(
- self.TokenKind.TEXT, stream.pop_mem_str(len(self.wrapper))
- )
- )
- state = _State.VAR
- stream.push_mem()
- else:
- result.append(
- self.Token.from_mem_str(
- self.TokenKind.VAR,
- stream.pop_mem_str(len(self.wrapper)),
- )
- )
- state = _State.TEXT
- stream.push_mem()
-
- continue
-
- stream.skip(1)
-
- if state is _State.VAR:
- raise ParseError("Text ended without closing variable.", self, text)
-
- mem_str = stream.pop_mem_str()
- if len(mem_str.text) != 0:
- result.append(self.Token.from_mem_str(self.TokenKind.TEXT, mem_str))
-
- return result
diff --git a/store/works/python/cru/service/__init__.py b/store/works/python/cru/service/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/store/works/python/cru/service/__init__.py
+++ /dev/null
diff --git a/store/works/python/cru/service/__main__.py b/store/works/python/cru/service/__main__.py
deleted file mode 100644
index 2a0268b..0000000
--- a/store/works/python/cru/service/__main__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import sys
-
-from cru import CruException
-
-from ._app import create_app
-
-
-def main():
- app = create_app()
- app.run_command()
-
-
-if __name__ == "__main__":
- version_info = sys.version_info
- if not (version_info.major == 3 and version_info.minor >= 11):
- print("This application requires Python 3.11 or later.", file=sys.stderr)
- sys.exit(1)
-
- try:
- main()
- except CruException as e:
- user_message = e.get_user_message()
- if user_message is not None:
- print(f"Error: {user_message}")
- exit(1)
- else:
- raise
diff --git a/store/works/python/cru/service/_app.py b/store/works/python/cru/service/_app.py
deleted file mode 100644
index b4c6271..0000000
--- a/store/works/python/cru/service/_app.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from ._base import (
- AppBase,
- CommandDispatcher,
- PathCommandProvider,
-)
-from ._template import TemplateManager
-from ._nginx import NginxManager
-from ._gen_cmd import GenCmdProvider
-
-APP_ID = "crupest"
-
-
-class App(AppBase):
- def __init__(self):
- super().__init__(APP_ID, f"{APP_ID}-service")
- self.add_feature(PathCommandProvider())
- self.add_feature(TemplateManager())
- self.add_feature(NginxManager())
- self.add_feature(GenCmdProvider())
- self.add_feature(CommandDispatcher())
-
- def run_command(self):
- command_dispatcher = self.get_feature(CommandDispatcher)
- command_dispatcher.run_command()
-
-
-def create_app() -> App:
- app = App()
- app.setup()
- return app
diff --git a/store/works/python/cru/service/_base.py b/store/works/python/cru/service/_base.py
deleted file mode 100644
index e1eee70..0000000
--- a/store/works/python/cru/service/_base.py
+++ /dev/null
@@ -1,400 +0,0 @@
-from __future__ import annotations
-
-from argparse import ArgumentParser, Namespace
-from abc import ABC, abstractmethod
-import argparse
-import os
-from pathlib import Path
-from typing import TypeVar, overload
-
-from cru import CruException, CruLogicError
-
-_Feature = TypeVar("_Feature", bound="AppFeatureProvider")
-
-
-class AppError(CruException):
- pass
-
-
-class AppFeatureError(AppError):
- def __init__(self, message, feature: type | str, *args, **kwargs):
- super().__init__(message, *args, **kwargs)
- self._feature = feature
-
- @property
- def feature(self) -> type | str:
- return self._feature
-
-
-class AppPathError(CruException):
- def __init__(self, message, _path: str | Path, *args, **kwargs):
- super().__init__(message, *args, **kwargs)
- self._path = str(_path)
-
- @property
- def path(self) -> str:
- return self._path
-
-
-class AppPath(ABC):
- def __init__(self, id: str, is_dir: bool, description: str) -> None:
- self._is_dir = is_dir
- self._id = id
- self._description = description
-
- @property
- @abstractmethod
- def parent(self) -> AppPath | None: ...
-
- @property
- @abstractmethod
- def app(self) -> AppBase: ...
-
- @property
- def id(self) -> str:
- return self._id
-
- @property
- def description(self) -> str:
- return self._description
-
- @property
- def is_dir(self) -> bool:
- return self._is_dir
-
- @property
- @abstractmethod
- def full_path(self) -> Path: ...
-
- @property
- def full_path_str(self) -> str:
- return str(self.full_path)
-
- def check_parents(self, must_exist: bool = False) -> bool:
- for p in reversed(self.full_path.parents):
- if not p.exists() and not must_exist:
- return False
- if not p.is_dir():
- raise AppPathError("Parents' path must be a dir.", self.full_path)
- return True
-
- def check_self(self, must_exist: bool = False) -> bool:
- if not self.check_parents(must_exist):
- return False
- if not self.full_path.exists():
- if not must_exist:
- return False
- raise AppPathError("Not exist.", self.full_path)
- if self.is_dir:
- if not self.full_path.is_dir():
- raise AppPathError("Should be a directory, but not.", self.full_path)
- else:
- return True
- else:
- if not self.full_path.is_file():
- raise AppPathError("Should be a file, but not.", self.full_path)
- else:
- return True
-
- def ensure(self, create_file: bool = False) -> None:
- e = self.check_self(False)
- if not e:
- os.makedirs(self.full_path.parent, exist_ok=True)
- if self.is_dir:
- os.mkdir(self.full_path)
- elif create_file:
- with open(self.full_path, "w") as f:
- f.write("")
-
- def read_text(self) -> str:
- if self.is_dir:
- raise AppPathError("Can't read text of a dir.", self.full_path)
- self.check_self()
- return self.full_path.read_text()
-
- def add_subpath(
- self,
- name: str,
- is_dir: bool,
- /,
- id: str | None = None,
- description: str = "",
- ) -> AppFeaturePath:
- return self.app._add_path(name, is_dir, self, id, description)
-
- @property
- def app_relative_path(self) -> Path:
- return self.full_path.relative_to(self.app.root.full_path)
-
-
-class AppFeaturePath(AppPath):
- def __init__(
- self,
- parent: AppPath,
- name: str,
- is_dir: bool,
- /,
- id: str | None = None,
- description: str = "",
- ) -> None:
- super().__init__(id or name, is_dir, description)
- self._name = name
- self._parent = parent
-
- @property
- def name(self) -> str:
- return self._name
-
- @property
- def parent(self) -> AppPath:
- return self._parent
-
- @property
- def app(self) -> AppBase:
- return self.parent.app
-
- @property
- def full_path(self) -> Path:
- return Path(self.parent.full_path, self.name).resolve()
-
-
-class AppRootPath(AppPath):
- def __init__(self, app: AppBase, path: Path):
- super().__init__(f"/{id}", True, f"Application {id} root path.")
- self._app = app
- self._full_path = path.resolve()
-
- @property
- def parent(self) -> None:
- return None
-
- @property
- def app(self) -> AppBase:
- return self._app
-
- @property
- def full_path(self) -> Path:
- return self._full_path
-
-
-class AppFeatureProvider(ABC):
- def __init__(self, name: str, /, app: AppBase | None = None):
- super().__init__()
- self._name = name
- self._app = app if app else AppBase.get_instance()
-
- @property
- def app(self) -> AppBase:
- return self._app
-
- @property
- def name(self) -> str:
- return self._name
-
- @abstractmethod
- def setup(self) -> None: ...
-
-
-class AppCommandFeatureProvider(AppFeatureProvider):
- @abstractmethod
- def get_command_info(self) -> tuple[str, str]: ...
-
- @abstractmethod
- def setup_arg_parser(self, arg_parser: ArgumentParser): ...
-
- @abstractmethod
- def run_command(self, args: Namespace) -> None: ...
-
-
-class PathCommandProvider(AppCommandFeatureProvider):
- def __init__(self) -> None:
- super().__init__("path-command-provider")
-
- def setup(self):
- pass
-
- def get_command_info(self):
- return ("path", "Get information about paths used by app.")
-
- def setup_arg_parser(self, arg_parser: ArgumentParser) -> None:
- subparsers = arg_parser.add_subparsers(
- dest="path_command", metavar="PATH_COMMAND"
- )
- _list_parser = subparsers.add_parser(
- "list", help="list special paths used by app"
- )
-
- def run_command(self, args: Namespace) -> None:
- if args.path_command is None or args.path_command == "list":
- for path in self.app.paths:
- print(
- f"{path.app_relative_path.as_posix()}{'/' if path.is_dir else ''}: {path.description}"
- )
-
-
-class CommandDispatcher(AppFeatureProvider):
- def __init__(self) -> None:
- super().__init__("command-dispatcher")
-
- def setup_arg_parser(self) -> None:
- self._map: dict[str, AppCommandFeatureProvider] = {}
- arg_parser = argparse.ArgumentParser(
- description="Service management",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- )
- subparsers = arg_parser.add_subparsers(
- dest="command",
- help="The management command to execute.",
- metavar="COMMAND",
- )
- for feature in self.app.features:
- if isinstance(feature, AppCommandFeatureProvider):
- info = feature.get_command_info()
- command_subparser = subparsers.add_parser(info[0], help=info[1])
- feature.setup_arg_parser(command_subparser)
- self._map[info[0]] = feature
- self._arg_parser = arg_parser
-
- def setup(self):
- self._parsed_args = self.arg_parser.parse_args()
-
- @property
- def arg_parser(self) -> argparse.ArgumentParser:
- return self._arg_parser
-
- @property
- def command_map(self) -> dict[str, AppCommandFeatureProvider]:
- return self._map
-
- @property
- def program_args(self) -> argparse.Namespace:
- return self._parsed_args
-
- def run_command(self) -> None:
- args = self.program_args
- if args.command is None:
- self.arg_parser.print_help()
- return
- self.command_map[args.command].run_command(args)
-
-
-class AppBase:
- _instance: AppBase | None = None
-
- @staticmethod
- def get_instance() -> AppBase:
- if AppBase._instance is None:
- raise AppError("App instance not initialized")
- return AppBase._instance
-
- def __init__(self, app_id: str, name: str):
- AppBase._instance = self
- self._app_id = app_id
- self._name = name
- self._features: list[AppFeatureProvider] = []
- self._paths: list[AppFeaturePath] = []
-
- def setup(self) -> None:
- command_dispatcher = self.get_feature(CommandDispatcher)
- command_dispatcher.setup_arg_parser()
- self._root = AppRootPath(self, Path(self._ensure_env("CRUPEST_PROJECT_DIR")))
- self._data_dir = self._root.add_subpath(
- self._ensure_env("CRUPEST_DATA_DIR"), True, id="data"
- )
- self._services_dir = self._root.add_subpath(
- self._ensure_env("CRUPEST_SERVICES_DIR"), True, id="CRUPEST_SERVICES_DIR"
- )
- for feature in self.features:
- feature.setup()
- for path in self.paths:
- path.check_self()
-
- @property
- def app_id(self) -> str:
- return self._app_id
-
- @property
- def name(self) -> str:
- return self._name
-
- def _ensure_env(self, env_name: str) -> str:
- value = os.getenv(env_name)
- if value is None:
- raise AppError(f"Environment variable {env_name} not set")
- return value
-
- @property
- def root(self) -> AppRootPath:
- return self._root
-
- @property
- def data_dir(self) -> AppFeaturePath:
- return self._data_dir
-
- @property
- def services_dir(self) -> AppFeaturePath:
- return self._services_dir
-
- @property
- def app_initialized(self) -> bool:
- return self.data_dir.check_self()
-
- @property
- def features(self) -> list[AppFeatureProvider]:
- return self._features
-
- @property
- def paths(self) -> list[AppFeaturePath]:
- return self._paths
-
- def add_feature(self, feature: _Feature) -> _Feature:
- for f in self.features:
- if f.name == feature.name:
- raise AppFeatureError(
- f"Duplicate feature name: {feature.name}.", feature.name
- )
- self._features.append(feature)
- return feature
-
- def _add_path(
- self,
- name: str,
- is_dir: bool,
- /,
- parent: AppPath | None = None,
- id: str | None = None,
- description: str = "",
- ) -> AppFeaturePath:
- p = AppFeaturePath(
- parent or self.root, name, is_dir, id=id, description=description
- )
- self._paths.append(p)
- return p
-
- @overload
- def get_feature(self, feature: str) -> AppFeatureProvider: ...
-
- @overload
- def get_feature(self, feature: type[_Feature]) -> _Feature: ...
-
- def get_feature(
- self, feature: str | type[_Feature]
- ) -> AppFeatureProvider | _Feature:
- if isinstance(feature, str):
- for f in self._features:
- if f.name == feature:
- return f
- elif isinstance(feature, type):
- for f in self._features:
- if isinstance(f, feature):
- return f
- else:
- raise CruLogicError("Argument must be the name of feature or its class.")
-
- raise AppFeatureError(f"Feature {feature} not found.", feature)
-
- def get_path(self, name: str) -> AppFeaturePath:
- for p in self._paths:
- if p.id == name or p.name == name:
- return p
- raise AppPathError(f"Application path {name} not found.", name)
diff --git a/store/works/python/cru/service/_gen_cmd.py b/store/works/python/cru/service/_gen_cmd.py
deleted file mode 100644
index f51d65f..0000000
--- a/store/works/python/cru/service/_gen_cmd.py
+++ /dev/null
@@ -1,200 +0,0 @@
-from dataclasses import dataclass, replace
-from typing import TypeAlias
-
-from ._base import AppCommandFeatureProvider
-from ._nginx import NginxManager
-
-_Str_Or_Cmd_List: TypeAlias = str | list["_Cmd"]
-
-
-@dataclass
-class _Cmd:
- name: str
- desc: str
- cmd: _Str_Or_Cmd_List
-
- def clean(self) -> "_Cmd":
- if isinstance(self.cmd, list):
- return replace(
- self,
- cmd=[cmd.clean() for cmd in self.cmd],
- )
- elif isinstance(self.cmd, str):
- return replace(self, cmd=self.cmd.strip())
- else:
- raise ValueError("Unexpected type for cmd.")
-
- def generate_text(
- self,
- info_only: bool,
- *,
- parent: str | None = None,
- ) -> str:
- if parent is None:
- tag = "COMMAND"
- long_name = self.name
- indent = ""
- else:
- tag = "SUBCOMMAND"
- long_name = f"{parent}.{self.name}"
- indent = " "
-
- if info_only:
- return f"{indent}[{long_name}]: {self.desc}"
-
- text = f"--- {tag}[{long_name}]: {self.desc}"
- if isinstance(self.cmd, str):
- text += "\n" + self.cmd
- elif isinstance(self.cmd, list):
- for sub in self.cmd:
- text += "\n" * 2 + sub.generate_text(info_only, parent=self.name)
- else:
- raise ValueError("Unexpected type for cmd.")
-
- lines: list[str] = []
- for line in text.splitlines():
- if len(line) == 0:
- lines.append("")
- else:
- lines.append(indent + line)
- text = "\n".join(lines)
-
- return text
-
-
-_docker_uninstall = _Cmd(
- "uninstall",
- "uninstall apt docker",
- """
-for pkg in docker.io docker-doc docker-compose \
-podman-docker containerd runc; \
-do sudo apt-get remove $pkg; done
-""",
-)
-
-_docker_apt_certs = _Cmd(
- "apt-certs",
- "prepare apt certs",
- """
-sudo apt-get update
-sudo apt-get install ca-certificates curl
-sudo install -m 0755 -d /etc/apt/keyrings
-""",
-)
-
-_docker_docker_certs = _Cmd(
- "docker-certs",
- "add docker certs",
- """
-sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
--o /etc/apt/keyrings/docker.asc
-sudo chmod a+r /etc/apt/keyrings/docker.asc
-""",
-)
-
-_docker_apt_repo = _Cmd(
- "apt-repo",
- "add docker apt repo",
- """
-echo \\
- "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
-https://download.docker.com/linux/debian \\
- $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \\
- sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
-""",
-)
-
-_docker_install = _Cmd(
- "install",
- "update apt and install docker",
- """
-sudo apt-get update
-sudo apt-get install docker-ce docker-ce-cli containerd.io \
-docker-buildx-plugin docker-compose-plugin
-""",
-)
-
-_docker_setup = _Cmd(
- "setup",
- "setup system for docker",
- """
-sudo systemctl enable docker
-sudo systemctl start docker
-sudo groupadd -f docker
-sudo usermod -aG docker $USER
-# Remember to log out and log back in for the group changes to take effect
-""",
-)
-
-_docker = _Cmd(
- "install-docker",
- "install docker for a fresh new system",
- [
- _docker_uninstall,
- _docker_apt_certs,
- _docker_docker_certs,
- _docker_apt_repo,
- _docker_install,
- _docker_setup,
- ],
-)
-
-_update_blog = _Cmd(
- "update-blog",
- "re-generate blog pages",
- """
-docker exec -it blog /scripts/update.bash
-""",
-)
-
-_git_user = _Cmd(
- "git-user",
- "add/set git server user and password",
- """
-docker run -it --rm -v "$ps_file:/user-info" httpd htpasswd "/user-info" [username]
-""",
-)
-
-
-class GenCmdProvider(AppCommandFeatureProvider):
- def __init__(self) -> None:
- super().__init__("gen-cmd-provider")
- self._cmds: dict[str, _Cmd] = {}
- self._register_cmds(_docker, _update_blog, _git_user)
-
- def _register_cmd(self, cmd: "_Cmd"):
- self._cmds[cmd.name] = cmd.clean()
-
- def _register_cmds(self, *cmds: "_Cmd"):
- for c in cmds:
- self._register_cmd(c)
-
- def setup(self):
- pass
-
- def get_command_info(self):
- return ("gen-cmd", "Get commands of running external cli tools.")
-
- def setup_arg_parser(self, arg_parser):
- subparsers = arg_parser.add_subparsers(
- dest="gen_cmd", metavar="GEN_CMD_COMMAND"
- )
- certbot_parser = subparsers.add_parser("certbot", help="print certbot commands")
- certbot_parser.add_argument(
- "-t", "--test", action="store_true", help="run certbot in test mode"
- )
- for cmd in self._cmds.values():
- subparsers.add_parser(cmd.name, help=cmd.desc)
-
- def _print_cmd(self, name: str):
- print(self._cmds[name].generate_text(False))
-
- def run_command(self, args):
- if args.gen_cmd is None or args.gen_cmd == "list":
- print("[certbot]: certbot ssl cert commands")
- for cmd in self._cmds.values():
- print(cmd.generate_text(True))
- elif args.gen_cmd == "certbot":
- self.app.get_feature(NginxManager).print_all_certbot_commands(args.test)
- else:
- self._print_cmd(args.gen_cmd)
diff --git a/store/works/python/cru/service/_nginx.py b/store/works/python/cru/service/_nginx.py
deleted file mode 100644
index 87cff6d..0000000
--- a/store/works/python/cru/service/_nginx.py
+++ /dev/null
@@ -1,263 +0,0 @@
-from argparse import Namespace
-from enum import Enum, auto
-import re
-import subprocess
-from typing import TypeAlias
-
-from cru import CruInternalError
-
-from ._base import AppCommandFeatureProvider
-from ._template import TemplateManager
-
-
-class CertbotAction(Enum):
- CREATE = auto()
- EXPAND = auto()
- SHRINK = auto()
- RENEW = auto()
-
-
-class NginxManager(AppCommandFeatureProvider):
- CertbotAction: TypeAlias = CertbotAction
-
- def __init__(self) -> None:
- super().__init__("nginx-manager")
- self._domains_cache: list[str] | None = None
-
- def setup(self) -> None:
- pass
-
- @property
- def _template_manager(self) -> TemplateManager:
- return self.app.get_feature(TemplateManager)
-
- @property
- def root_domain(self) -> str:
- return self._template_manager.get_domain()
-
- @property
- def domains(self) -> list[str]:
- if self._domains_cache is None:
- self._domains_cache = self._get_domains()
- return self._domains_cache
-
- @property
- def subdomains(self) -> list[str]:
- suffix = "." + self.root_domain
- return [d[: -len(suffix)] for d in self.domains if d.endswith(suffix)]
-
- def _get_domains_from_text(self, text: str) -> set[str]:
- domains: set[str] = set()
- regex = re.compile(r"server_name\s+(\S+)\s*;")
- for match in regex.finditer(text):
- domains.add(match[1])
- return domains
-
- def _join_generated_nginx_conf_text(self) -> str:
- result = ""
- for path, text in self._template_manager.generate():
- if "nginx" in str(path):
- result += text
- return result
-
- def _get_domains(self) -> list[str]:
- text = self._join_generated_nginx_conf_text()
- domains = self._get_domains_from_text(text)
- domains.remove(self.root_domain)
- return [self.root_domain, *domains]
-
- def _print_domains(self) -> None:
- for domain in self.domains:
- print(domain)
-
- def _certbot_command(
- self,
- action: CertbotAction | str,
- test: bool,
- *,
- docker=True,
- standalone=None,
- email=None,
- agree_tos=True,
- ) -> str:
- if isinstance(action, str):
- action = CertbotAction[action.upper()]
-
- command_args = []
-
- add_domain_option = True
- if action is CertbotAction.CREATE:
- if standalone is None:
- standalone = True
- command_action = "certonly"
- elif action in [CertbotAction.EXPAND, CertbotAction.SHRINK]:
- if standalone is None:
- standalone = False
- command_action = "certonly"
- elif action is CertbotAction.RENEW:
- if standalone is None:
- standalone = False
- add_domain_option = False
- command_action = "renew"
- else:
- raise CruInternalError("Invalid certbot action.")
-
- data_dir = self.app.data_dir.full_path.as_posix()
-
- if not docker:
- command_args.append("certbot")
- else:
- command_args.extend(
- [
- "docker run -it --rm --name certbot",
- f'-v "{data_dir}/certbot/certs:/etc/letsencrypt"',
- f'-v "{data_dir}/certbot/data:/var/lib/letsencrypt"',
- ]
- )
- if standalone:
- command_args.append('-p "0.0.0.0:80:80"')
- else:
- command_args.append(f'-v "{data_dir}/certbot/webroot:/var/www/certbot"')
-
- command_args.append("certbot/certbot")
-
- command_args.append(command_action)
-
- command_args.append(f"--cert-name {self.root_domain}")
-
- if standalone:
- command_args.append("--standalone")
- else:
- command_args.append("--webroot -w /var/www/certbot")
-
- if add_domain_option:
- command_args.append(" ".join([f"-d {domain}" for domain in self.domains]))
-
- if email is not None:
- command_args.append(f"--email {email}")
-
- if agree_tos:
- command_args.append("--agree-tos")
-
- if test:
- command_args.append("--test-cert --dry-run")
-
- return " ".join(command_args)
-
- def print_all_certbot_commands(self, test: bool):
- print("### COMMAND: (standalone) create certs")
- print(
- self._certbot_command(
- CertbotAction.CREATE,
- test,
- email=self._template_manager.get_email(),
- )
- )
- print()
- print("### COMMAND: (webroot+nginx) expand or shrink certs")
- print(
- self._certbot_command(
- CertbotAction.EXPAND,
- test,
- email=self._template_manager.get_email(),
- )
- )
- print()
- print("### COMMAND: (webroot+nginx) renew certs")
- print(
- self._certbot_command(
- CertbotAction.RENEW,
- test,
- email=self._template_manager.get_email(),
- )
- )
-
- @property
- def _cert_path_str(self) -> str:
- return str(
- self.app.data_dir.full_path
- / "certbot/certs/live"
- / self.root_domain
- / "fullchain.pem"
- )
-
- def get_command_info(self):
- return "nginx", "Manage nginx related things."
-
- def setup_arg_parser(self, arg_parser):
- subparsers = arg_parser.add_subparsers(
- dest="nginx_command", required=True, metavar="NGINX_COMMAND"
- )
- _list_parser = subparsers.add_parser("list", help="list domains")
- certbot_parser = subparsers.add_parser("certbot", help="print certbot commands")
- certbot_parser.add_argument(
- "--no-test",
- action="store_true",
- help="remove args making certbot run in test mode",
- )
-
- def run_command(self, args: Namespace) -> None:
- if args.nginx_command == "list":
- self._print_domains()
- elif args.nginx_command == "certbot":
- self.print_all_certbot_commands(not args.no_test)
-
- def _generate_dns_zone(
- self,
- ip: str,
- /,
- ttl: str | int = 600,
- *,
- enable_mail: bool = True,
- dkim: str | None = None,
- ) -> str:
- # TODO: Not complete and test now.
- root_domain = self.root_domain
- result = f"$ORIGIN {root_domain}.\n\n"
- result += "; A records\n"
- result += f"@ {ttl} IN A {ip}\n"
- for subdomain in self.subdomains:
- result += f"{subdomain} {ttl} IN A {ip}\n"
-
- if enable_mail:
- result += "\n; MX records\n"
- result += f"@ {ttl} IN MX 10 mail.{root_domain}.\n"
- result += "\n; SPF record\n"
- result += f'@ {ttl} IN TXT "v=spf1 mx ~all"\n'
- if dkim is not None:
- result += "\n; DKIM record\n"
- result += f'mail._domainkey {ttl} IN TEXT "{dkim}"'
- result += "\n; DMARC record\n"
- dmarc_options = [
- "v=DMARC1",
- "p=none",
- f"rua=mailto:dmarc.report@{root_domain}",
- f"ruf=mailto:dmarc.report@{root_domain}",
- "sp=none",
- "ri=86400",
- ]
- result += f'_dmarc {ttl} IN TXT "{"; ".join(dmarc_options)}"\n'
- return result
-
- def _get_dkim_from_mailserver(self) -> str | None:
- # TODO: Not complete and test now.
- dkim_path = (
- self.app.data_dir.full_path
- / "dms/config/opendkim/keys"
- / self.root_domain
- / "mail.txt"
- )
- if not dkim_path.exists():
- return None
-
- p = subprocess.run(["sudo", "cat", dkim_path], capture_output=True, check=True)
- value = ""
- for match in re.finditer('"(.*)"', p.stdout.decode("utf-8")):
- value += match.group(1)
- return value
-
- def _generate_dns_zone_with_dkim(self, ip: str, /, ttl: str | int = 600) -> str:
- # TODO: Not complete and test now.
- return self._generate_dns_zone(
- ip, ttl, enable_mail=True, dkim=self._get_dkim_from_mailserver()
- )
diff --git a/store/works/python/cru/service/_template.py b/store/works/python/cru/service/_template.py
deleted file mode 100644
index 22c1d21..0000000
--- a/store/works/python/cru/service/_template.py
+++ /dev/null
@@ -1,228 +0,0 @@
-from argparse import Namespace
-from pathlib import Path
-import shutil
-from typing import NamedTuple
-import graphlib
-
-from cru import CruException
-from cru.parsing import SimpleLineVarParser
-from cru.template import TemplateTree, CruStrWrapperTemplate
-
-from ._base import AppCommandFeatureProvider, AppFeaturePath
-
-
-class _Config(NamedTuple):
- text: str
- config: dict[str, str]
-
-
-class _GeneratedConfig(NamedTuple):
- base: _Config
- private: _Config
- merged: _Config
-
-
-class _PreConfig(NamedTuple):
- base: _Config
- private: _Config
- config: dict[str, str]
-
- @staticmethod
- def create(base: _Config, private: _Config) -> "_PreConfig":
- return _PreConfig(base, private, {**base.config, **private.config})
-
- def _merge(self, generated: _Config):
- text = (
- "\n".join(
- [
- self.private.text.strip(),
- self.base.text.strip(),
- generated.text.strip(),
- ]
- )
- + "\n"
- )
- config = {**self.config, **generated.config}
- return _GeneratedConfig(self.base, self.private, _Config(text, config))
-
-
-class _Template(NamedTuple):
- config: CruStrWrapperTemplate
- config_vars: set[str]
- tree: TemplateTree
-
-
-class TemplateManager(AppCommandFeatureProvider):
- def __init__(self):
- super().__init__("template-manager")
-
- def setup(self) -> None:
- self._base_config_file = self.app.services_dir.add_subpath("base-config", False)
- self._private_config_file = self.app.data_dir.add_subpath("config", False)
- self._template_config_file = self.app.services_dir.add_subpath(
- "config.template", False
- )
- self._templates_dir = self.app.services_dir.add_subpath("templates", True)
- self._generated_dir = self.app.services_dir.add_subpath("generated", True)
-
- self._config_parser = SimpleLineVarParser()
-
- def _read_pre(app_path: AppFeaturePath) -> _Config:
- text = app_path.read_text()
- config = self._read_config(text)
- return _Config(text, config)
-
- base = _read_pre(self._base_config_file)
- private = _read_pre(self._private_config_file)
- self._preconfig = _PreConfig.create(base, private)
-
- self._generated: _GeneratedConfig | None = None
-
- template_config_text = self._template_config_file.read_text()
- self._template_config = self._read_config(template_config_text)
-
- self._template = _Template(
- CruStrWrapperTemplate(template_config_text),
- set(self._template_config.keys()),
- TemplateTree(
- lambda text: CruStrWrapperTemplate(text),
- self.templates_dir.full_path_str,
- ),
- )
-
- self._real_required_vars = (
- self._template.config_vars | self._template.tree.variables
- ) - self._template.config_vars
- lacks = self._real_required_vars - self._preconfig.config.keys()
- self._lack_vars = lacks if len(lacks) > 0 else None
-
- def _read_config_entry_names(self, text: str) -> set[str]:
- return set(entry.key for entry in self._config_parser.parse(text))
-
- def _read_config(self, text: str) -> dict[str, str]:
- return {entry.key: entry.value for entry in self._config_parser.parse(text)}
-
- @property
- def templates_dir(self) -> AppFeaturePath:
- return self._templates_dir
-
- @property
- def generated_dir(self) -> AppFeaturePath:
- return self._generated_dir
-
- def get_domain(self) -> str:
- return self._preconfig.config["CRUPEST_DOMAIN"]
-
- def get_email(self) -> str:
- return self._preconfig.config["CRUPEST_EMAIL"]
-
- def _generate_template_config(self, config: dict[str, str]) -> dict[str, str]:
- entry_templates = {
- key: CruStrWrapperTemplate(value)
- for key, value in self._template_config.items()
- }
- sorter = graphlib.TopologicalSorter(
- config
- | {key: template.variables for key, template in entry_templates.items()}
- )
-
- vars: dict[str, str] = config.copy()
- for _ in sorter.static_order():
- del_keys = []
- for key, template in entry_templates.items():
- new = template.generate_partial(vars)
- if not new.has_variables:
- vars[key] = new.generate({})
- del_keys.append(key)
- else:
- entry_templates[key] = new
- for key in del_keys:
- del entry_templates[key]
- assert len(entry_templates) == 0
- return {key: value for key, value in vars.items() if key not in config}
-
- def _generate_config(self) -> _GeneratedConfig:
- if self._generated is not None:
- return self._generated
- if self._lack_vars is not None:
- raise CruException(f"Required vars are not defined: {self._lack_vars}.")
- config = self._generate_template_config(self._preconfig.config)
- text = self._template.config.generate(self._preconfig.config | config)
- self._generated = self._preconfig._merge(_Config(text, config))
- return self._generated
-
- def generate(self) -> list[tuple[Path, str]]:
- config = self._generate_config()
- return [
- (Path("config"), config.merged.text),
- *self._template.tree.generate(config.merged.config),
- ]
-
- def _generate_files(self, dry_run: bool) -> None:
- result = self.generate()
- if not dry_run:
- if self.generated_dir.full_path.exists():
- shutil.rmtree(self.generated_dir.full_path)
- for path, text in result:
- des = self.generated_dir.full_path / path
- des.parent.mkdir(parents=True, exist_ok=True)
- with open(des, "w") as f:
- f.write(text)
-
- def get_command_info(self):
- return ("template", "Manage templates.")
-
- def _print_file_lists(self) -> None:
- print(f"[{self._template.config.variable_count}]", "config")
- for path, template in self._template.tree.templates:
- print(f"[{template.variable_count}]", path.as_posix())
-
- def _print_vars(self, required: bool) -> None:
- for var in self._template.config.variables:
- print(f"[config] {var}")
- for var in self._template.tree.variables:
- if not (required and var in self._template.config_vars):
- print(f"[template] {var}")
-
- def _run_check_vars(self) -> None:
- if self._lack_vars is not None:
- print("Lacks:")
- for var in self._lack_vars:
- print(var)
-
- def setup_arg_parser(self, arg_parser):
- subparsers = arg_parser.add_subparsers(
- dest="template_command", required=True, metavar="TEMPLATE_COMMAND"
- )
- _list_parser = subparsers.add_parser("list", help="list templates")
- vars_parser = subparsers.add_parser(
- "vars", help="list variables used in all templates"
- )
- vars_parser.add_argument(
- "-r",
- "--required",
- help="only list really required one.",
- action="store_true",
- )
- _check_vars_parser = subparsers.add_parser(
- "check-vars",
- help="check if required vars are set",
- )
- generate_parser = subparsers.add_parser("generate", help="generate templates")
- generate_parser.add_argument(
- "--no-dry-run", action="store_true", help="generate and write target files"
- )
-
- def run_command(self, args: Namespace) -> None:
- if args.template_command == "list":
- self._print_file_lists()
- elif args.template_command == "vars":
- self._print_vars(args.required)
- elif args.template_command == "generate":
- dry_run = not args.no_dry_run
- self._generate_files(dry_run)
- if dry_run:
- print("Dry run successfully.")
- print(
- f"Will delete dir {self.generated_dir.full_path_str} if it exists."
- )
diff --git a/store/works/python/cru/system.py b/store/works/python/cru/system.py
deleted file mode 100644
index f321717..0000000
--- a/store/works/python/cru/system.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import os.path
-import re
-
-
-def check_debian_derivative_version(name: str) -> None | str:
- if not os.path.isfile("/etc/os-release"):
- return None
- with open("/etc/os-release", "r") as f:
- content = f.read()
- if f"ID={name}" not in content:
- return None
- m = re.search(r'VERSION_ID="(.+)"', content)
- if m is None:
- return None
- return m.group(1)
-
-
-def check_ubuntu_version() -> None | str:
- return check_debian_derivative_version("ubuntu")
-
-
-def check_debian_version() -> None | str:
- return check_debian_derivative_version("debian")
diff --git a/store/works/python/cru/template.py b/store/works/python/cru/template.py
deleted file mode 100644
index 3a70337..0000000
--- a/store/works/python/cru/template.py
+++ /dev/null
@@ -1,209 +0,0 @@
-from abc import ABCMeta, abstractmethod
-from collections.abc import Callable, Mapping
-from pathlib import Path
-from string import Template
-from typing import Generic, Self, TypeVar
-
-from ._iter import CruIterator
-from ._error import CruException
-
-from .parsing import StrWrapperVarParser
-
-
-class CruTemplateError(CruException):
- pass
-
-
-class CruTemplateBase(metaclass=ABCMeta):
- def __init__(self, text: str):
- self._text = text
- self._variables: set[str] | None = None
-
- @abstractmethod
- def _get_variables(self) -> set[str]:
- raise NotImplementedError()
-
- @property
- def text(self) -> str:
- return self._text
-
- @property
- def variables(self) -> set[str]:
- if self._variables is None:
- self._variables = self._get_variables()
- return self._variables
-
- @property
- def variable_count(self) -> int:
- return len(self.variables)
-
- @property
- def has_variables(self) -> bool:
- return self.variable_count > 0
-
- @abstractmethod
- def _do_generate(self, mapping: dict[str, str]) -> str:
- raise NotImplementedError()
-
- def _generate_partial(
- self, mapping: Mapping[str, str], allow_unused: bool = True
- ) -> str:
- values = dict(mapping)
- if not allow_unused and not len(set(values.keys() - self.variables)) != 0:
- raise CruTemplateError("Unused variables.")
- return self._do_generate(values)
-
- def generate_partial(
- self, mapping: Mapping[str, str], allow_unused: bool = True
- ) -> Self:
- return self.__class__(self._generate_partial(mapping, allow_unused))
-
- def generate(self, mapping: Mapping[str, str], allow_unused: bool = True) -> str:
- values = dict(mapping)
- if len(self.variables - values.keys()) != 0:
- raise CruTemplateError(
- f"Missing variables: {self.variables - values.keys()} ."
- )
- return self._generate_partial(values, allow_unused)
-
-
-class CruTemplate(CruTemplateBase):
- def __init__(self, prefix: str, text: str):
- super().__init__(text)
- self._prefix = prefix
- self._template = Template(text)
-
- def _get_variables(self) -> set[str]:
- return (
- CruIterator(self._template.get_identifiers())
- .filter(lambda i: i.startswith(self.prefix))
- .to_set()
- )
-
- @property
- def prefix(self) -> str:
- return self._prefix
-
- @property
- def py_template(self) -> Template:
- return self._template
-
- @property
- def all_variables(self) -> set[str]:
- return set(self._template.get_identifiers())
-
- def _do_generate(self, mapping: dict[str, str]) -> str:
- return self._template.safe_substitute(mapping)
-
-
-class CruStrWrapperTemplate(CruTemplateBase):
- def __init__(self, text: str, wrapper: str = "@@"):
- super().__init__(text)
- self._wrapper = wrapper
- self._tokens: StrWrapperVarParser.Result
-
- @property
- def wrapper(self) -> str:
- return self._wrapper
-
- def _get_variables(self):
- self._tokens = StrWrapperVarParser(self.wrapper).parse(self.text)
- return (
- self._tokens.cru_iter()
- .filter(lambda t: t.is_var)
- .map(lambda t: t.value)
- .to_set()
- )
-
- def _do_generate(self, mapping):
- return (
- self._tokens.cru_iter()
- .map(lambda t: mapping[t.value] if t.is_var else t.value)
- .join_str("")
- )
-
-
-_Template = TypeVar("_Template", bound=CruTemplateBase)
-
-
-class TemplateTree(Generic[_Template]):
- def __init__(
- self,
- template_generator: Callable[[str], _Template],
- source: str,
- *,
- template_file_suffix: str | None = ".template",
- ):
- """
- If template_file_suffix is not None, the files will be checked according to the
- suffix of the file name. If the suffix matches, the file will be regarded as a
- template file. Otherwise, it will be regarded as a non-template file.
- Content of template file must contain variables that need to be replaced, while
- content of non-template file may not contain any variables.
- If either case is false, it generally means whether the file is a template is
- wrongly handled.
- """
- self._template_generator = template_generator
- self._files: list[tuple[Path, _Template]] = []
- self._source = source
- self._template_file_suffix = template_file_suffix
- self._load()
-
- @property
- def templates(self) -> list[tuple[Path, _Template]]:
- return self._files
-
- @property
- def source(self) -> str:
- return self._source
-
- @property
- def template_file_suffix(self) -> str | None:
- return self._template_file_suffix
-
- @staticmethod
- def _scan_files(root: str) -> list[Path]:
- root_path = Path(root)
- result: list[Path] = []
- for path in root_path.glob("**/*"):
- if not path.is_file():
- continue
- path = path.relative_to(root_path)
- result.append(Path(path))
- return result
-
- def _load(self) -> None:
- files = self._scan_files(self.source)
- for file_path in files:
- template_file = Path(self.source) / file_path
- with open(template_file, "r") as f:
- content = f.read()
- template = self._template_generator(content)
- if self.template_file_suffix is not None:
- should_be_template = file_path.name.endswith(self.template_file_suffix)
- if should_be_template and not template.has_variables:
- raise CruTemplateError(
- f"Template file {file_path} has no variables."
- )
- elif not should_be_template and template.has_variables:
- raise CruTemplateError(f"Non-template {file_path} has variables.")
- self._files.append((file_path, template))
-
- @property
- def variables(self) -> set[str]:
- s = set()
- for _, template in self.templates:
- s.update(template.variables)
- return s
-
- def generate(self, variables: Mapping[str, str]) -> list[tuple[Path, str]]:
- result: list[tuple[Path, str]] = []
- for path, template in self.templates:
- if self.template_file_suffix is not None and path.name.endswith(
- self.template_file_suffix
- ):
- path = path.parent / (path.name[: -len(self.template_file_suffix)])
-
- text = template.generate(variables)
- result.append((path, text))
- return result
diff --git a/store/works/python/cru/tool.py b/store/works/python/cru/tool.py
deleted file mode 100644
index 377f5d7..0000000
--- a/store/works/python/cru/tool.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import shutil
-import subprocess
-from typing import Any
-from collections.abc import Iterable
-
-from ._error import CruException
-
-
-class CruExternalToolError(CruException):
- def __init__(self, message: str, tool: str, *args, **kwargs) -> None:
- super().__init__(message, *args, **kwargs)
- self._tool = tool
-
- @property
- def tool(self) -> str:
- return self._tool
-
-
-class CruExternalToolNotFoundError(CruExternalToolError):
- def __init__(self, message: str | None, tool: str, *args, **kwargs) -> None:
- super().__init__(
- message or f"Could not find binary for {tool}.", tool, *args, **kwargs
- )
-
-
-class CruExternalToolRunError(CruExternalToolError):
- def __init__(
- self,
- message: str,
- tool: str,
- tool_args: Iterable[str],
- tool_error: Any,
- *args,
- **kwargs,
- ) -> None:
- super().__init__(message, tool, *args, **kwargs)
- self._tool_args = list(tool_args)
- self._tool_error = tool_error
-
- @property
- def tool_args(self) -> list[str]:
- return self._tool_args
-
- @property
- def tool_error(self) -> Any:
- return self._tool_error
-
-
-class ExternalTool:
- def __init__(self, bin: str) -> None:
- self._bin = bin
-
- @property
- def bin(self) -> str:
- return self._bin
-
- @bin.setter
- def bin(self, value: str) -> None:
- self._bin = value
-
- @property
- def bin_path(self) -> str:
- real_bin = shutil.which(self.bin)
- if not real_bin:
- raise CruExternalToolNotFoundError(None, self.bin)
- return real_bin
-
- def run(
- self, *process_args: str, **subprocess_kwargs
- ) -> subprocess.CompletedProcess:
- try:
- return subprocess.run(
- [self.bin_path] + list(process_args), **subprocess_kwargs
- )
- except subprocess.CalledProcessError as e:
- raise CruExternalToolError("Subprocess failed.", self.bin) from e
- except OSError as e:
- raise CruExternalToolError("Failed to start subprocess", self.bin) from e
-
- def run_get_output(self, *process_args: str, **subprocess_kwargs) -> Any:
- process = self.run(*process_args, capture_output=True, **subprocess_kwargs)
- return process.stdout
diff --git a/store/works/python/cru/value.py b/store/works/python/cru/value.py
deleted file mode 100644
index 9c03219..0000000
--- a/store/works/python/cru/value.py
+++ /dev/null
@@ -1,292 +0,0 @@
-from __future__ import annotations
-
-import random
-import secrets
-import string
-import uuid
-from abc import abstractmethod, ABCMeta
-from collections.abc import Callable
-from typing import Any, ClassVar, TypeVar, Generic
-
-from ._error import CruException
-
-
-def _str_case_in(s: str, case: bool, str_list: list[str]) -> bool:
- if case:
- return s in str_list
- else:
- return s.lower() in [s.lower() for s in str_list]
-
-
-_T = TypeVar("_T")
-
-
-class CruValueTypeError(CruException):
- def __init__(
- self,
- message: str,
- value: Any,
- value_type: ValueType | None,
- *args,
- **kwargs,
- ):
- super().__init__(
- message,
- *args,
- **kwargs,
- )
- self._value = value
- self._value_type = value_type
-
- @property
- def value(self) -> Any:
- return self._value
-
- @property
- def value_type(self) -> ValueType | None:
- return self._value_type
-
-
-class ValueType(Generic[_T], metaclass=ABCMeta):
- def __init__(self, name: str, _type: type[_T]) -> None:
- self._name = name
- self._type = _type
-
- @property
- def name(self) -> str:
- return self._name
-
- @property
- def type(self) -> type[_T]:
- return self._type
-
- def check_value_type(self, value: Any) -> None:
- if not isinstance(value, self.type):
- raise CruValueTypeError("Type of value is wrong.", value, self)
-
- def _do_check_value(self, value: Any) -> _T:
- return value
-
- def check_value(self, value: Any) -> _T:
- self.check_value_type(value)
- return self._do_check_value(value)
-
- @abstractmethod
- def _do_check_str_format(self, s: str) -> None:
- raise NotImplementedError()
-
- def check_str_format(self, s: str) -> None:
- if not isinstance(s, str):
- raise CruValueTypeError("Try to check format on a non-str.", s, self)
- self._do_check_str_format(s)
-
- @abstractmethod
- def _do_convert_value_to_str(self, value: _T) -> str:
- raise NotImplementedError()
-
- def convert_value_to_str(self, value: _T) -> str:
- self.check_value(value)
- return self._do_convert_value_to_str(value)
-
- @abstractmethod
- def _do_convert_str_to_value(self, s: str) -> _T:
- raise NotImplementedError()
-
- def convert_str_to_value(self, s: str) -> _T:
- self.check_str_format(s)
- return self._do_convert_str_to_value(s)
-
- def check_value_or_try_convert_from_str(self, value_or_str: Any) -> _T:
- try:
- return self.check_value(value_or_str)
- except CruValueTypeError:
- if isinstance(value_or_str, str):
- return self.convert_str_to_value(value_or_str)
- else:
- raise
-
- def create_default_value(self) -> _T:
- return self.type()
-
-
-class TextValueType(ValueType[str]):
- def __init__(self) -> None:
- super().__init__("text", str)
-
- def _do_check_str_format(self, _s):
- return
-
- def _do_convert_value_to_str(self, value):
- return value
-
- def _do_convert_str_to_value(self, s):
- return s
-
-
-class IntegerValueType(ValueType[int]):
- def __init__(self) -> None:
- super().__init__("integer", int)
-
- def _do_check_str_format(self, s):
- try:
- int(s)
- except ValueError as e:
- raise CruValueTypeError("Invalid integer format.", s, self) from e
-
- def _do_convert_value_to_str(self, value):
- return str(value)
-
- def _do_convert_str_to_value(self, s):
- return int(s)
-
-
-class FloatValueType(ValueType[float]):
- def __init__(self) -> None:
- super().__init__("float", float)
-
- def _do_check_str_format(self, s):
- try:
- float(s)
- except ValueError as e:
- raise CruValueTypeError("Invalid float format.", s, self) from e
-
- def _do_convert_value_to_str(self, value):
- return str(value)
-
- def _do_convert_str_to_value(self, s):
- return float(s)
-
-
-class BooleanValueType(ValueType[bool]):
- DEFAULT_TRUE_LIST: ClassVar[list[str]] = ["true", "yes", "y", "on", "1"]
- DEFAULT_FALSE_LIST: ClassVar[list[str]] = ["false", "no", "n", "off", "0"]
-
- def __init__(
- self,
- *,
- case_sensitive=False,
- true_list: None | list[str] = None,
- false_list: None | list[str] = None,
- ) -> None:
- super().__init__("boolean", bool)
- self._case_sensitive = case_sensitive
- self._valid_true_strs: list[str] = (
- true_list or BooleanValueType.DEFAULT_TRUE_LIST
- )
- self._valid_false_strs: list[str] = (
- false_list or BooleanValueType.DEFAULT_FALSE_LIST
- )
-
- @property
- def case_sensitive(self) -> bool:
- return self._case_sensitive
-
- @property
- def valid_true_strs(self) -> list[str]:
- return self._valid_true_strs
-
- @property
- def valid_false_strs(self) -> list[str]:
- return self._valid_false_strs
-
- @property
- def valid_boolean_strs(self) -> list[str]:
- return self._valid_true_strs + self._valid_false_strs
-
- def _do_check_str_format(self, s):
- if not _str_case_in(s, self.case_sensitive, self.valid_boolean_strs):
- raise CruValueTypeError("Invalid boolean format.", s, self)
-
- def _do_convert_value_to_str(self, value):
- return self._valid_true_strs[0] if value else self._valid_false_strs[0]
-
- def _do_convert_str_to_value(self, s):
- return _str_case_in(s, self.case_sensitive, self._valid_true_strs)
-
- def create_default_value(self):
- return self.valid_false_strs[0]
-
-
-class EnumValueType(ValueType[str]):
- def __init__(self, valid_values: list[str], /, case_sensitive=False) -> None:
- super().__init__(f"enum({'|'.join(valid_values)})", str)
- self._case_sensitive = case_sensitive
- self._valid_values = valid_values
-
- @property
- def case_sensitive(self) -> bool:
- return self._case_sensitive
-
- @property
- def valid_values(self) -> list[str]:
- return self._valid_values
-
- def _do_check_value(self, value):
- self._do_check_str_format(value)
-
- def _do_check_str_format(self, s):
- if not _str_case_in(s, self.case_sensitive, self.valid_values):
- raise CruValueTypeError("Invalid enum value", s, self)
-
- def _do_convert_value_to_str(self, value):
- return value
-
- def _do_convert_str_to_value(self, s):
- return s
-
- def create_default_value(self):
- return self.valid_values[0]
-
-
-TEXT_VALUE_TYPE = TextValueType()
-INTEGER_VALUE_TYPE = IntegerValueType()
-BOOLEAN_VALUE_TYPE = BooleanValueType()
-
-
-class ValueGeneratorBase(Generic[_T], metaclass=ABCMeta):
- @abstractmethod
- def generate(self) -> _T:
- raise NotImplementedError()
-
- def __call__(self) -> _T:
- return self.generate()
-
-
-class ValueGenerator(ValueGeneratorBase[_T]):
- def __init__(self, generate_func: Callable[[], _T]) -> None:
- self._generate_func = generate_func
-
- @property
- def generate_func(self) -> Callable[[], _T]:
- return self._generate_func
-
- def generate(self) -> _T:
- return self._generate_func()
-
-
-class UuidValueGenerator(ValueGeneratorBase[str]):
- def generate(self):
- return str(uuid.uuid4())
-
-
-class RandomStringValueGenerator(ValueGeneratorBase[str]):
- def __init__(self, length: int, secure: bool) -> None:
- self._length = length
- self._secure = secure
-
- @property
- def length(self) -> int:
- return self._length
-
- @property
- def secure(self) -> bool:
- return self._secure
-
- def generate(self):
- random_func = secrets.choice if self._secure else random.choice
- characters = string.ascii_letters + string.digits
- random_string = "".join(random_func(characters) for _ in range(self._length))
- return random_string
-
-
-UUID_VALUE_GENERATOR = UuidValueGenerator()
diff --git a/store/works/python/poetry.lock b/store/works/python/poetry.lock
deleted file mode 100644
index 4338200..0000000
--- a/store/works/python/poetry.lock
+++ /dev/null
@@ -1,111 +0,0 @@
-# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
-
-[[package]]
-name = "mypy"
-version = "1.15.0"
-description = "Optional static typing for Python"
-optional = false
-python-versions = ">=3.9"
-groups = ["dev"]
-files = [
- {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
- {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
- {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
- {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
- {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
- {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
- {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
- {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
- {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
- {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
- {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
- {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
- {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
- {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
- {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
- {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
- {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
- {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
- {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
- {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
- {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
- {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
- {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
- {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
- {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
- {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
- {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
- {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
- {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
- {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
- {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
- {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
-]
-
-[package.dependencies]
-mypy_extensions = ">=1.0.0"
-typing_extensions = ">=4.6.0"
-
-[package.extras]
-dmypy = ["psutil (>=4.0)"]
-faster-cache = ["orjson"]
-install-types = ["pip"]
-mypyc = ["setuptools (>=50)"]
-reports = ["lxml"]
-
-[[package]]
-name = "mypy-extensions"
-version = "1.0.0"
-description = "Type system extensions for programs checked with the mypy type checker."
-optional = false
-python-versions = ">=3.5"
-groups = ["dev"]
-files = [
- {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
- {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
-]
-
-[[package]]
-name = "ruff"
-version = "0.9.6"
-description = "An extremely fast Python linter and code formatter, written in Rust."
-optional = false
-python-versions = ">=3.7"
-groups = ["dev"]
-files = [
- {file = "ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba"},
- {file = "ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504"},
- {file = "ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83"},
- {file = "ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc"},
- {file = "ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b"},
- {file = "ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e"},
- {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666"},
- {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5"},
- {file = "ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5"},
- {file = "ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217"},
- {file = "ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6"},
- {file = "ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897"},
- {file = "ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08"},
- {file = "ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656"},
- {file = "ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d"},
- {file = "ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa"},
- {file = "ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a"},
- {file = "ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9"},
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.12.2"
-description = "Backported and Experimental Type Hints for Python 3.8+"
-optional = false
-python-versions = ">=3.8"
-groups = ["dev"]
-files = [
- {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
- {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
-]
-
-[metadata]
-lock-version = "2.1"
-python-versions = "^3.11"
-content-hash = "674a21dbda993a1ee761e2e6e2f13ccece8289336a83fd0a154285eac48f3a76"
diff --git a/store/works/python/pyproject.toml b/store/works/python/pyproject.toml
deleted file mode 100644
index 28c753e..0000000
--- a/store/works/python/pyproject.toml
+++ /dev/null
@@ -1,19 +0,0 @@
-[project]
-name = "cru"
-version = "0.1.0"
-requires-python = ">=3.11"
-license = "MIT"
-
-[tool.poetry]
-package-mode = false
-
-[tool.poetry.group.dev.dependencies]
-mypy = "^1.13.0"
-ruff = "^0.9.6"
-
-[tool.ruff.lint]
-select = ["E", "F", "B"]
-
-[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"