Use dataclasses for the model
This commit is contained in:
		
							parent
							
								
									b1415cce16
								
							
						
					
					
						commit
						8f4d19c98e
					
				
					 6 changed files with 112 additions and 250 deletions
				
			
		|  | @ -1,3 +1,4 @@ | |||
| from pathlib import Path | ||||
| from typing import Any | ||||
| 
 | ||||
| 
 | ||||
|  | @ -5,10 +6,11 @@ SUPPORTED_MANIFEST = [ | |||
|     "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1", | ||||
|     "https://schemas.cute.engineering/stable/cutekit.manifest.project.v1", | ||||
|     "https://schemas.cute.engineering/stable/cutekit.manifest.target.v1", | ||||
| 
 | ||||
| ] | ||||
| 
 | ||||
| OSDK_MANIFEST_NOT_SUPPORTED = "OSDK manifests are not supported by CuteKit. Please use CuteKit manifest instead" | ||||
| OSDK_MANIFEST_NOT_SUPPORTED = ( | ||||
|     "OSDK manifests are not supported by CuteKit. Please use CuteKit manifest instead" | ||||
| ) | ||||
| 
 | ||||
| UNSUPORTED_MANIFEST = { | ||||
|     "https://schemas.cute.engineering/stable/osdk.manifest.component.v1": OSDK_MANIFEST_NOT_SUPPORTED, | ||||
|  | @ -20,14 +22,16 @@ UNSUPORTED_MANIFEST = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| def ensureSupportedManifest(manifest: Any, path: str): | ||||
| def ensureSupportedManifest(manifest: Any, path: Path): | ||||
|     if "$schema" not in manifest: | ||||
|         raise RuntimeError(f"Missing $schema in {path}") | ||||
| 
 | ||||
|     if manifest["$schema"] in UNSUPORTED_MANIFEST: | ||||
|         raise RuntimeError( | ||||
|             f"Unsupported manifest schema {manifest['$schema']} in {path}: {UNSUPORTED_MANIFEST[manifest['$schema']]}") | ||||
|             f"Unsupported manifest schema {manifest['$schema']} in {path}: {UNSUPORTED_MANIFEST[manifest['$schema']]}" | ||||
|         ) | ||||
| 
 | ||||
|     if manifest["$schema"] not in SUPPORTED_MANIFEST: | ||||
|         raise RuntimeError( | ||||
|             f"Unsupported manifest schema {manifest['$schema']} in {path}") | ||||
|             f"Unsupported manifest schema {manifest['$schema']} in {path}" | ||||
|         ) | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import os | ||||
| import sys | ||||
| 
 | ||||
| VERSION = (0, 5, 4) | ||||
| VERSION = (0, 6, 0, "dev") | ||||
| VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if len(VERSION) >= 4 else ''}" | ||||
| MODULE_DIR = os.path.dirname(os.path.realpath(__file__)) | ||||
| ARGV0 = os.path.basename(sys.argv[0]) | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ class ComponentInstance: | |||
|         return self.manifest.id | ||||
| 
 | ||||
|     def isLib(self): | ||||
|         return self.manifest.type == model.Type.LIB | ||||
|         return self.manifest.type == model.Kind.LIB | ||||
| 
 | ||||
|     def objdir(self) -> str: | ||||
|         return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj") | ||||
|  | @ -88,7 +88,7 @@ class ComponentInstance: | |||
|     def cinclude(self) -> str: | ||||
|         if "cpp-root-include" in self.manifest.props: | ||||
|             return self.manifest.dirname() | ||||
|         elif self.manifest.type == model.Type.LIB: | ||||
|         elif self.manifest.type == model.Kind.LIB: | ||||
|             return str(Path(self.manifest.dirname()).parent) | ||||
|         else: | ||||
|             return "" | ||||
|  | @ -131,7 +131,7 @@ class Context(IContext): | |||
| 
 | ||||
|     def hashid(self) -> str: | ||||
|         return utils.hash( | ||||
|             (self.target.props, [self.tools[t].toJson() for t in self.tools]) | ||||
|             (self.target.props, [self.tools[t].to_dict() for t in self.tools]) | ||||
|         )[0:8] | ||||
| 
 | ||||
|     def builddir(self) -> str: | ||||
|  | @ -154,14 +154,19 @@ def loadAllTargets() -> list[model.Target]: | |||
|     ret = [] | ||||
|     for entry in paths: | ||||
|         files = shell.find(entry, ["*.json"]) | ||||
|         ret += list(map(lambda path: model.Target(jexpr.evalRead(path), path), files)) | ||||
|         ret += list( | ||||
|             map( | ||||
|                 lambda path: model.Manifest.load(Path(path)).ensureType(model.Target), | ||||
|                 files, | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|     return ret | ||||
| 
 | ||||
| 
 | ||||
| def loadProject(path: str) -> model.Project: | ||||
|     path = os.path.join(path, "project.json") | ||||
|     return model.Project(jexpr.evalRead(path), path) | ||||
|     return model.Manifest.load(Path(path)).ensureType(model.Project) | ||||
| 
 | ||||
| 
 | ||||
| def loadTarget(id: str) -> model.Target: | ||||
|  | @ -175,7 +180,12 @@ def loadAllComponents() -> list[model.Component]: | |||
|     files = shell.find(const.SRC_DIR, ["manifest.json"]) | ||||
|     files += shell.find(const.EXTERN_DIR, ["manifest.json"]) | ||||
| 
 | ||||
|     return list(map(lambda path: model.Component(jexpr.evalRead(path), path), files)) | ||||
|     return list( | ||||
|         map( | ||||
|             lambda path: model.Manifest.load(Path(path)).ensureType(model.Component), | ||||
|             files, | ||||
|         ) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def filterDisabled( | ||||
|  | @ -242,7 +252,7 @@ def resolveDeps( | |||
| 
 | ||||
|     enabled, unresolvedReason, resolved = resolveInner(componentSpec) | ||||
| 
 | ||||
|     return enabled, unresolvedReason, resolved | ||||
|     return enabled, unresolvedReason, utils.uniq(resolved) | ||||
| 
 | ||||
| 
 | ||||
| def instanciate( | ||||
|  | @ -250,7 +260,10 @@ def instanciate( | |||
| ) -> Optional[ComponentInstance]: | ||||
|     manifest = next(filter(lambda c: c.id == componentSpec, components)) | ||||
|     wildcards = set(chain(*map(lambda rule: rule.fileIn, rules.rules.values()))) | ||||
|     sources = shell.find(manifest.subdirs, list(wildcards), recusive=False) | ||||
|     dirs = [manifest.dirname()] + list( | ||||
|         map(lambda d: os.path.join(manifest.dirname(), d), manifest.subdirs) | ||||
|     ) | ||||
|     sources = shell.find(dirs, list(wildcards), recusive=False) | ||||
| 
 | ||||
|     res = shell.find(os.path.join(manifest.dirname(), "res")) | ||||
| 
 | ||||
|  | @ -299,9 +312,7 @@ def contextFor(targetSpec: str, props: model.Props = {}) -> Context: | |||
|     for toolSpec in target.tools: | ||||
|         tool = target.tools[toolSpec] | ||||
| 
 | ||||
|         tools[toolSpec] = model.Tool( | ||||
|             strict=False, cmd=tool.cmd, args=tool.args, files=tool.files | ||||
|         ) | ||||
|         tools[toolSpec] = model.Tool(cmd=tool.cmd, args=tool.args, files=tool.files) | ||||
| 
 | ||||
|         tools[toolSpec].args += rules.rules[toolSpec].args | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import os | ||||
| import json | ||||
| from pathlib import Path | ||||
| 
 | ||||
| from typing import Any, cast, Callable, Final | ||||
| from . import shell, compat | ||||
|  | @ -9,8 +10,8 @@ Builtin = Callable[..., Json] | |||
| 
 | ||||
| BUILTINS: Final[dict[str, Builtin]] = { | ||||
|     "uname": lambda arg, ctx: getattr(shell.uname(), arg).lower(), | ||||
|     "include": lambda arg, ctx: evalRead(arg, compatibilityCheck=False), | ||||
|     "evalRead": lambda arg, ctx: evalRead(arg, compatibilityCheck=False), | ||||
|     "include": lambda arg, ctx: evalRead(arg), | ||||
|     "evalRead": lambda arg, ctx: evalRead(arg), | ||||
|     "join": lambda lhs, rhs, ctx: cast( | ||||
|         Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs | ||||
|     ), | ||||
|  | @ -27,7 +28,7 @@ BUILTINS: Final[dict[str, Builtin]] = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| def eval(jexpr: Json, filePath: str) -> Json: | ||||
| def eval(jexpr: Json, filePath: Path) -> Json: | ||||
|     if isinstance(jexpr, dict): | ||||
|         result = {} | ||||
|         for k in cast(dict[str, Json], jexpr): | ||||
|  | @ -49,7 +50,7 @@ def eval(jexpr: Json, filePath: str) -> Json: | |||
|         return jexpr | ||||
| 
 | ||||
| 
 | ||||
| def read(path: str) -> Json: | ||||
| def read(path: Path) -> Json: | ||||
|     try: | ||||
|         with open(path, "r") as f: | ||||
|             return json.load(f) | ||||
|  | @ -57,8 +58,6 @@ def read(path: str) -> Json: | |||
|         raise RuntimeError(f"Failed to read {path}") | ||||
| 
 | ||||
| 
 | ||||
| def evalRead(path: str, compatibilityCheck: bool = True) -> Json: | ||||
| def evalRead(path: Path) -> Json: | ||||
|     data = read(path) | ||||
|     if compatibilityCheck: | ||||
|         compat.ensureSupportedManifest(data, path) | ||||
|     return eval(data, path) | ||||
|  |  | |||
							
								
								
									
										284
									
								
								cutekit/model.py
									
										
									
									
									
								
							
							
						
						
									
										284
									
								
								cutekit/model.py
									
										
									
									
									
								
							|  | @ -1,18 +1,21 @@ | |||
| import os | ||||
| import logging | ||||
| 
 | ||||
| from enum import Enum | ||||
| from typing import Any | ||||
| from pathlib import Path | ||||
| from . import jexpr | ||||
| 
 | ||||
| from enum import Enum | ||||
| from typing import Any, Type, cast | ||||
| from pathlib import Path | ||||
| from dataclasses_json import DataClassJsonMixin, config | ||||
| from dataclasses import dataclass, field | ||||
| 
 | ||||
| from . import jexpr, compat, utils | ||||
| 
 | ||||
| _logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| Props = dict[str, Any] | ||||
| 
 | ||||
| 
 | ||||
| class Type(Enum): | ||||
| class Kind(Enum): | ||||
|     UNKNOWN = "unknown" | ||||
|     PROJECT = "project" | ||||
|     TARGET = "target" | ||||
|  | @ -20,115 +23,46 @@ class Type(Enum): | |||
|     EXE = "exe" | ||||
| 
 | ||||
| 
 | ||||
| class Manifest: | ||||
|     id: str = "" | ||||
|     type: Type = Type.UNKNOWN | ||||
|     path: str = "" | ||||
| @dataclass | ||||
| class Manifest(DataClassJsonMixin): | ||||
|     id: str | ||||
|     type: Kind = field(default=Kind.UNKNOWN) | ||||
|     path: str = field(default="") | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         json: jexpr.Json = None, | ||||
|         path: str = "", | ||||
|         strict: bool = True, | ||||
|         **kwargs: Any, | ||||
|     ): | ||||
|         if json is not None: | ||||
|             if "id" not in json: | ||||
|                 raise RuntimeError("Missing id") | ||||
|     @staticmethod | ||||
|     def parse(path: Path, data: dict[str, Any]) -> "Manifest": | ||||
|         compat.ensureSupportedManifest(data, path) | ||||
|         kind = Kind(data["type"]) | ||||
|         del data["$schema"] | ||||
|         obj = KINDS[kind].from_dict(data) | ||||
|         obj.path = str(path) | ||||
|         return obj | ||||
| 
 | ||||
|             self.id = json["id"] | ||||
| 
 | ||||
|             if "type" not in json and strict: | ||||
|                 raise RuntimeError("Missing type") | ||||
| 
 | ||||
|             self.type = Type(json["type"]) | ||||
| 
 | ||||
|             self.path = path | ||||
|         elif strict: | ||||
|             raise RuntimeError("Missing json") | ||||
| 
 | ||||
|         for key in kwargs: | ||||
|             setattr(self, key, kwargs[key]) | ||||
| 
 | ||||
|     def toJson(self) -> jexpr.Json: | ||||
|         return {"id": self.id, "type": self.type.value, "path": self.path} | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return f"Manifest(id={self.id}, type={self.type}, path={self.path})" | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"Manifest({id})" | ||||
|     @staticmethod | ||||
|     def load(path: Path) -> "Manifest": | ||||
|         return Manifest.parse(path, jexpr.evalRead(path)) | ||||
| 
 | ||||
|     def dirname(self) -> str: | ||||
|         return os.path.dirname(self.path) | ||||
| 
 | ||||
| 
 | ||||
| class Extern: | ||||
|     git: str = "" | ||||
|     tag: str = "" | ||||
| 
 | ||||
|     def __init__(self, json: jexpr.Json = None, strict: bool = True, **kwargs: Any): | ||||
|         if json is not None: | ||||
|             if "git" not in json and strict: | ||||
|                 raise RuntimeError("Missing git") | ||||
| 
 | ||||
|             self.git = json["git"] | ||||
| 
 | ||||
|             if "tag" not in json and strict: | ||||
|                 raise RuntimeError("Missing tag") | ||||
| 
 | ||||
|             self.tag = json["tag"] | ||||
|         elif strict: | ||||
|             raise RuntimeError("Missing json") | ||||
| 
 | ||||
|         for key in kwargs: | ||||
|             setattr(self, key, kwargs[key]) | ||||
| 
 | ||||
|     def toJson(self) -> jexpr.Json: | ||||
|         return {"git": self.git, "tag": self.tag} | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return f"Extern(git={self.git}, tag={self.tag})" | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"Extern({self.git})" | ||||
|     def ensureType(self, t: Type[utils.T]) -> utils.T: | ||||
|         if not isinstance(self, t): | ||||
|             raise RuntimeError( | ||||
|                 f"{self.path} should be a {type.__name__} manifest but is a {self.__class__.__name__} manifest" | ||||
|             ) | ||||
|         return cast(utils.T, self) | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Extern(DataClassJsonMixin): | ||||
|     git: str | ||||
|     tag: str | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Project(Manifest): | ||||
|     description: str = "" | ||||
|     extern: dict[str, Extern] = {} | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         json: jexpr.Json = None, | ||||
|         path: str = "", | ||||
|         strict: bool = True, | ||||
|         **kwargs: Any, | ||||
|     ): | ||||
|         if json is not None: | ||||
|             if "description" not in json and strict: | ||||
|                 raise RuntimeError("Missing description") | ||||
| 
 | ||||
|             self.description = json["description"] | ||||
| 
 | ||||
|             self.extern = {k: Extern(v) for k, v in json.get("extern", {}).items()} | ||||
|         elif strict: | ||||
|             raise RuntimeError("Missing json") | ||||
| 
 | ||||
|         super().__init__(json, path, strict, **kwargs) | ||||
| 
 | ||||
|     def toJson(self) -> jexpr.Json: | ||||
|         return { | ||||
|             **super().toJson(), | ||||
|             "description": self.description, | ||||
|             "extern": {k: v.toJson() for k, v in self.extern.items()}, | ||||
|         } | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return f"ProjectManifest(id={self.id}, type={self.type}, path={self.path}, description={self.description}, extern={self.extern})" | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"ProjectManifest({self.id})" | ||||
|     description: str = field(default="(No description)") | ||||
|     extern: dict[str, Extern] = field(default_factory=dict) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def root() -> str | None: | ||||
|  | @ -149,80 +83,21 @@ class Project(Manifest): | |||
|         os.chdir(path) | ||||
| 
 | ||||
| 
 | ||||
| class Tool: | ||||
|     cmd: str = "" | ||||
|     args: list[str] = [] | ||||
|     files: list[str] = [] | ||||
| 
 | ||||
|     def __init__(self, json: jexpr.Json = None, strict: bool = True, **kwargs: Any): | ||||
|         if json is not None: | ||||
|             if "cmd" not in json and strict: | ||||
|                 raise RuntimeError("Missing cmd") | ||||
| 
 | ||||
|             self.cmd = json.get("cmd", self.cmd) | ||||
| 
 | ||||
|             if "args" not in json and strict: | ||||
|                 raise RuntimeError("Missing args") | ||||
| 
 | ||||
|             self.args = json.get("args", []) | ||||
| 
 | ||||
|             self.files = json.get("files", []) | ||||
|         elif strict: | ||||
|             raise RuntimeError("Missing json") | ||||
| 
 | ||||
|         for key in kwargs: | ||||
|             setattr(self, key, kwargs[key]) | ||||
| 
 | ||||
|     def toJson(self) -> jexpr.Json: | ||||
|         return {"cmd": self.cmd, "args": self.args, "files": self.files} | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return f"Tool(cmd={self.cmd}, args={self.args}, files={self.files})" | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"Tool({self.cmd})" | ||||
| @dataclass | ||||
| class Tool(DataClassJsonMixin): | ||||
|     cmd: str = field(default="") | ||||
|     args: list[str] = field(default_factory=list) | ||||
|     files: list[str] = field(default_factory=list) | ||||
| 
 | ||||
| 
 | ||||
| Tools = dict[str, Tool] | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Target(Manifest): | ||||
|     props: Props | ||||
|     tools: Tools | ||||
|     routing: dict[str, str] | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         json: jexpr.Json = None, | ||||
|         path: str = "", | ||||
|         strict: bool = True, | ||||
|         **kwargs: Any, | ||||
|     ): | ||||
|         if json is not None: | ||||
|             if "props" not in json and strict: | ||||
|                 raise RuntimeError("Missing props") | ||||
| 
 | ||||
|             self.props = json["props"] | ||||
| 
 | ||||
|             if "tools" not in json and strict: | ||||
|                 raise RuntimeError("Missing tools") | ||||
| 
 | ||||
|             self.tools = {k: Tool(v) for k, v in json["tools"].items()} | ||||
| 
 | ||||
|             self.routing = json.get("routing", {}) | ||||
| 
 | ||||
|         super().__init__(json, path, strict, **kwargs) | ||||
| 
 | ||||
|     def toJson(self) -> jexpr.Json: | ||||
|         return { | ||||
|             **super().toJson(), | ||||
|             "props": self.props, | ||||
|             "tools": {k: v.toJson() for k, v in self.tools.items()}, | ||||
|             "routing": self.routing, | ||||
|         } | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"TargetManifest({self.id})" | ||||
|     props: Props = field(default_factory=dict) | ||||
|     tools: Tools = field(default_factory=dict) | ||||
|     routing: dict[str, str] = field(default_factory=dict) | ||||
| 
 | ||||
|     def route(self, componentSpec: str): | ||||
|         return ( | ||||
|  | @ -250,54 +125,15 @@ class Target(Manifest): | |||
|         return defines | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Component(Manifest): | ||||
|     decription: str = "(No description)" | ||||
|     props: Props = {} | ||||
|     tools: Tools = {} | ||||
|     enableIf: dict[str, list[Any]] = {} | ||||
|     requires: list[str] = [] | ||||
|     provides: list[str] = [] | ||||
|     subdirs: list[str] = [] | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         json: jexpr.Json = None, | ||||
|         path: str = "", | ||||
|         strict: bool = True, | ||||
|         **kwargs: Any, | ||||
|     ): | ||||
|         if json is not None: | ||||
|             self.decription = json.get("description", self.decription) | ||||
|             self.props = json.get("props", self.props) | ||||
|             self.tools = { | ||||
|                 k: Tool(v, strict=False) for k, v in json.get("tools", {}).items() | ||||
|             } | ||||
|             self.enableIf = json.get("enableIf", self.enableIf) | ||||
|             self.requires = json.get("requires", self.requires) | ||||
|             self.provides = json.get("provides", self.provides) | ||||
|             self.subdirs = list( | ||||
|                 map( | ||||
|                     lambda x: os.path.join(os.path.dirname(path), x), | ||||
|                     json.get("subdirs", [""]), | ||||
|                 ) | ||||
|             ) | ||||
| 
 | ||||
|         super().__init__(json, path, strict, **kwargs) | ||||
| 
 | ||||
|     def toJson(self) -> jexpr.Json: | ||||
|         return { | ||||
|             **super().toJson(), | ||||
|             "description": self.decription, | ||||
|             "props": self.props, | ||||
|             "tools": {k: v.toJson() for k, v in self.tools.items()}, | ||||
|             "enableIf": self.enableIf, | ||||
|             "requires": self.requires, | ||||
|             "provides": self.provides, | ||||
|             "subdirs": self.subdirs, | ||||
|         } | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"ComponentManifest({self.id})" | ||||
|     decription: str = field(default="(No description)") | ||||
|     props: Props = field(default_factory=dict) | ||||
|     tools: Tools = field(default_factory=dict) | ||||
|     enableIf: dict[str, list[Any]] = field(default_factory=dict) | ||||
|     requires: list[str] = field(default_factory=list) | ||||
|     provides: list[str] = field(default_factory=list) | ||||
|     subdirs: list[str] = field(default_factory=list) | ||||
| 
 | ||||
|     def isEnabled(self, target: Target) -> tuple[bool, str]: | ||||
|         for k, v in self.enableIf.items(): | ||||
|  | @ -316,3 +152,11 @@ class Component(Manifest): | |||
|                 ) | ||||
| 
 | ||||
|         return True, "" | ||||
| 
 | ||||
| 
 | ||||
| KINDS: dict[Kind, Type[Manifest]] = { | ||||
|     Kind.PROJECT: Project, | ||||
|     Kind.TARGET: Target, | ||||
|     Kind.LIB: Component, | ||||
|     Kind.EXE: Component, | ||||
| } | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ from typing import Any, TypeVar, cast, Optional, Union | |||
| import json | ||||
| import hashlib | ||||
| 
 | ||||
| T = TypeVar('T') | ||||
| T = TypeVar("T") | ||||
| 
 | ||||
| 
 | ||||
| def uniq(l: list[str]) -> list[str]: | ||||
|     result: list[str] = [] | ||||
| def uniq(l: list[T]) -> list[T]: | ||||
|     result: list[T] = [] | ||||
|     for i in l: | ||||
|         if i in result: | ||||
|             result.remove(i) | ||||
|  | @ -15,7 +15,9 @@ def uniq(l: list[str]) -> list[str]: | |||
|     return result | ||||
| 
 | ||||
| 
 | ||||
| def hash(obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = None) -> str: | ||||
| def hash( | ||||
|     obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = None | ||||
| ) -> str: | ||||
|     toHash = {} | ||||
|     if len(keys) == 0: | ||||
|         toHash = obj | ||||
|  | @ -28,7 +30,7 @@ def hash(obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = | |||
| 
 | ||||
| 
 | ||||
| def camelCase(s: str) -> str: | ||||
|     s = ''.join(x for x in s.title() if x != '_' and x != '-') | ||||
|     s = "".join(x for x in s.title() if x != "_" and x != "-") | ||||
|     s = s[0].lower() + s[1:] | ||||
|     return s | ||||
| 
 | ||||
|  | @ -60,4 +62,6 @@ def asList(i: Optional[Union[T, list[T]]]) -> list[T]: | |||
| 
 | ||||
| 
 | ||||
| def isNewer(path1: str, path2: str) -> bool: | ||||
|     return not os.path.exists(path2) or os.path.getmtime(path1) > os.path.getmtime(path2) | ||||
|     return not os.path.exists(path2) or os.path.getmtime(path1) > os.path.getmtime( | ||||
|         path2 | ||||
|     ) | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue