ts-backend-check 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,5 @@
1
1
  # SPDX-License-Identifier: AGPL-3.0-or-later
2
- __version__ = "1.0.0"
2
+ from ts_backend_check.checker import TypeChecker
3
+ from ts_backend_check.cli.main import main
3
4
 
4
- from .checker import TypeChecker
5
- from .cli import cli
6
-
7
- __all__ = ["cli", "TypeChecker"]
5
+ __all__ = ["main", "TypeChecker"]
@@ -5,9 +5,9 @@ Main module for checking Django models against TypeScript types.
5
5
 
6
6
  from typing import Dict, List, Set
7
7
 
8
- from .django_parser import extract_model_fields
9
- from .typescript_parser import TypeScriptParser
10
- from .utils import snake_to_camel
8
+ from ts_backend_check.parsers.django_parser import extract_model_fields
9
+ from ts_backend_check.parsers.typescript_parser import TypeScriptParser
10
+ from ts_backend_check.utils import snake_to_camel
11
11
 
12
12
 
13
13
  class TypeChecker:
@@ -149,8 +149,8 @@ class TypeChecker:
149
149
  """
150
150
  potential_names = TypeChecker._generate_potential_names(model_name)
151
151
  return (
152
- f"\nNo matching TypeScript interface found for model: {model_name}\n"
153
- f"Searched for interfaces: {', '.join(potential_names)}"
152
+ f"\nNo matching TypeScript interface found for model: {model_name}"
153
+ f"\nSearched for interfaces: {', '.join(potential_names)}"
154
154
  )
155
155
 
156
156
  @staticmethod
@@ -178,8 +178,8 @@ class TypeChecker:
178
178
  """
179
179
  camel_field = snake_to_camel(input_str=field)
180
180
  return (
181
- f"\nField '{field}' (camelCase: '{camel_field}') from model '{model_name}' "
182
- f"is missing in TypeScript types.\n"
183
- f"Expected to find in interface(s): {', '.join(interfaces.keys())}\n"
184
- f"To ignore this field, add a comment that references it like: '// Note: {camel_field} is backend only'"
181
+ f"\nField '{field}' (camelCase: '{camel_field}') from model '{model_name}' is missing in TypeScript types."
182
+ f"\nExpected to find in interface(s): {', '.join(interfaces.keys())}"
183
+ f"\nTo ignore this field, add a comment that references it like: '// Note: {camel_field} is backend only'"
184
+ "\n"
185
185
  )
@@ -1,8 +1,4 @@
1
1
  # SPDX-License-Identifier: AGPL-3.0-or-later
2
- """
3
- CLI package for ts-backend-check.
4
- """
2
+ from ts_backend_check.cli.main import main
5
3
 
6
- from .main import cli
7
-
8
- __all__ = ["cli"]
4
+ __all__ = ["main"]
@@ -0,0 +1,111 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """
3
+ Functionality to check TypeScript interfaces for fields that should be optional based on Django models.
4
+ """
5
+
6
+ import ast
7
+ from pathlib import Path
8
+ from typing import Dict, Set
9
+
10
+ from rich.console import Console
11
+
12
+ from ts_backend_check.parsers.django_parser import DjangoModelVisitor
13
+
14
+ ROOT_DIR = Path.cwd()
15
+ console = Console()
16
+
17
+
18
+ class BlankParser(DjangoModelVisitor):
19
+ """
20
+ AST visitor to extract blank fields from Django models based on DjangoModelVisitor.
21
+ """
22
+
23
+ def __init__(self) -> None:
24
+ super().__init__()
25
+ self.blank_models: Dict[str, Set[str]] = {}
26
+
27
+ def visit_Assign(self, node: ast.Assign) -> None:
28
+ """
29
+ Check assignment statements within a class which accepts blank fields.
30
+
31
+ Parameters
32
+ ----------
33
+ node : ast.Assign
34
+ An assignment definition from Python AST (Abstract Syntax Tree).
35
+ It represents an assignment statement (e.g., x = 42).
36
+ """
37
+ if not self.current_model:
38
+ return
39
+
40
+ for target in node.targets:
41
+ if (
42
+ (
43
+ isinstance(target, ast.Name)
44
+ and not target.id.startswith("_")
45
+ and isinstance(node.value, ast.Call)
46
+ and hasattr(node.value.func, "attr")
47
+ )
48
+ and any(
49
+ field_type in node.value.func.attr
50
+ for field_type in self.DJANGO_FIELD_TYPES
51
+ )
52
+ and any(
53
+ kw.arg == "blank"
54
+ and isinstance(kw.value, ast.Constant)
55
+ and kw.value.value is True
56
+ for kw in node.value.keywords
57
+ )
58
+ ):
59
+ if self.current_model not in self.blank_models:
60
+ self.blank_models[self.current_model] = set()
61
+
62
+ self.blank_models[self.current_model].add(target.id)
63
+
64
+
65
+ def check_blank(file_path: str) -> Dict[str, Set[str]]:
66
+ """
67
+ Function to extract fields from Django models file which accepts blank values.
68
+
69
+ Parameters
70
+ ----------
71
+ file_path : str
72
+ A models.py file that defines Django models.
73
+
74
+ Returns
75
+ -------
76
+ Dict[str, Set[str]]
77
+ The fields from the models file extracted into a dictionary for future processing.
78
+ """
79
+ model_path = ROOT_DIR / file_path
80
+
81
+ if model_path.is_file():
82
+ with open(model_path, "r", encoding="utf-8") as f:
83
+ content = f.read().strip()
84
+ # Skip any empty lines at the beginning.
85
+ while content.startswith("\n"):
86
+ content = content[1:]
87
+
88
+ try:
89
+ tree = ast.parse(content)
90
+
91
+ except SyntaxError as e:
92
+ raise SyntaxError(
93
+ f"Failed to parse {model_path}. Make sure it's a valid Python file. Error: {str(e)}"
94
+ ) from e
95
+
96
+ parser = BlankParser()
97
+ parser.visit(tree)
98
+
99
+ if len(parser.blank_models) == 0:
100
+ console.print("[green]No models have any blank fields specified.[green]")
101
+
102
+ else:
103
+ for k, v in parser.blank_models.items():
104
+ console.print(
105
+ f"[yellow]Model {k} has fields {sorted(v)} set as optional."
106
+ )
107
+
108
+ else:
109
+ print("Check the path entered.")
110
+
111
+ return parser.blank_models
@@ -0,0 +1,164 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """
3
+ Configure cli to run based on a YAML configuration file.
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ from yaml import dump
9
+
10
+ YAML_CONFIG_FILE_PATH = (
11
+ Path(__file__).parent.parent.parent.parent / ".ts-backend-check.yaml"
12
+ )
13
+
14
+ PROJECT_ROOT_PATH = Path(__file__).parent.parent.parent.parent
15
+
16
+
17
+ def configure_paths() -> None:
18
+ """
19
+ Function to receive paths from user.
20
+ """
21
+ config_options = {}
22
+ while True:
23
+ print("\n--- Adding new model/interface configuration ---")
24
+
25
+ key = input(
26
+ "Enter the model-interface type (eg: 'auth', 'orgs', 'groups'): "
27
+ ).strip()
28
+ if not key:
29
+ print("Key cannot be empty. Please try again.")
30
+ continue
31
+
32
+ # Get backend path.
33
+ while True:
34
+ backend_path = input("Enter the path for Django models.py file: ").strip()
35
+ if not backend_path:
36
+ print("Path cannot be empty.")
37
+ continue
38
+
39
+ if path_exists(backend_path):
40
+ break
41
+
42
+ print(f"File not found: {PROJECT_ROOT_PATH / backend_path}")
43
+ print("Please check the path and try again.")
44
+
45
+ # Get frontend path.
46
+ while True:
47
+ frontend_path = input("Enter path to TypeScript interface file: ").strip()
48
+ if not frontend_path:
49
+ print("Path cannot be empty. Please try again.")
50
+ continue
51
+
52
+ if path_exists(frontend_path):
53
+ break
54
+
55
+ print(f"File not found: {PROJECT_ROOT_PATH / frontend_path}")
56
+ print("Please check the path and try again.")
57
+
58
+ config_options[key] = {
59
+ "backend_model_path": backend_path,
60
+ "frontend_interface_path": frontend_path,
61
+ }
62
+
63
+ print(f"✓ Added configuration for '{key}'")
64
+
65
+ continue_config = input(
66
+ "Do you wish to add another model/interface configuration? (y/[n])"
67
+ ).strip()
68
+
69
+ if continue_config.lower() in ["n", ""]:
70
+ if config_options:
71
+ write_config(config_options)
72
+ print(
73
+ f"\n✓ Configuration complete! Added {len(config_options)} configuration(s)."
74
+ )
75
+ break
76
+
77
+ if config_options:
78
+ write_config(config_options)
79
+ print(
80
+ f"\n✓ Configuration complete! Added {len(config_options)} configuration(s)."
81
+ )
82
+
83
+ else:
84
+ print("\nNo configurations added.")
85
+
86
+
87
+ def path_exists(path: str) -> bool:
88
+ """
89
+ Check if path entered by the user exists withing the filesystem.
90
+
91
+ Parameters
92
+ ----------
93
+ path : str
94
+ Path should be entered as a string from the user.
95
+
96
+ Returns
97
+ -------
98
+ bool
99
+ Return true or false based on if path exists.
100
+ """
101
+ full_path = Path(__file__).parent.parent.parent.parent / path
102
+ if Path(full_path).is_file():
103
+ return True
104
+
105
+ return False
106
+
107
+
108
+ def write_config(config: dict[str, dict[str, str]]) -> None:
109
+ """
110
+ Function to write into .ts-backend-check.yaml file.
111
+
112
+ Parameters
113
+ ----------
114
+ config : dict[str, dict[str, str]]
115
+ Passing a dictionary as key str with another dict as value.
116
+ """
117
+ try:
118
+ options = f"""# Configuration file for ts-backend-check validation.
119
+ # See https://github.com/activist-org/ts-backend-check for details.
120
+
121
+ # Paths:
122
+ {dump(config)}
123
+
124
+ """
125
+ with open(YAML_CONFIG_FILE_PATH, "w") as file:
126
+ file.write(options)
127
+
128
+ except IOError as e:
129
+ print(f"Error while writing config file: {e}")
130
+
131
+
132
+ def create_config() -> None:
133
+ """
134
+ Main function to create or update configuration.
135
+ """
136
+ print("ts-backend-check Configuration Setup")
137
+ print("=" * 40)
138
+
139
+ if YAML_CONFIG_FILE_PATH.is_file():
140
+ reconfig_choice = input(
141
+ "Configuration file exists. Do you want to re-configure your .ts-backend-check.yaml file? (y/[n]) "
142
+ )
143
+ if reconfig_choice.lower() in ["n", ""]:
144
+ print("Exiting without changes.")
145
+ return
146
+
147
+ print("Reconfiguring...")
148
+
149
+ else:
150
+ print("Creating new configuration file...")
151
+
152
+ try:
153
+ configure_paths()
154
+
155
+ except KeyboardInterrupt:
156
+ print("\n\nConfiguration cancelled by user.")
157
+
158
+ except Exception as e:
159
+ print(f"\nError during configuration: {e}")
160
+ print("Configuration cancelled.")
161
+
162
+
163
+ if __name__ == "__main__":
164
+ create_config()
@@ -3,56 +3,144 @@
3
3
  Setup and commands for the ts-backend-check command line interface.
4
4
  """
5
5
 
6
+ import argparse
6
7
  import sys
8
+ from argparse import ArgumentParser
9
+ from pathlib import Path
7
10
 
8
- import click
11
+ from rich import print as rprint
12
+ from rich.text import Text
9
13
 
10
- from ..checker import TypeChecker
14
+ from ts_backend_check.checker import TypeChecker
15
+ from ts_backend_check.cli.check_blank import check_blank
16
+ from ts_backend_check.cli.config import create_config
17
+ from ts_backend_check.cli.upgrade import upgrade_cli
18
+ from ts_backend_check.cli.version import get_version_message
11
19
 
20
+ ROOT_DIR = Path.cwd()
12
21
 
13
- @click.group()
14
- @click.version_option()
15
- def cli():
16
- """
17
- TS Backend Check is a Python package used to check TypeScript types against their corresponding backend models.
22
+
23
+ def main() -> None:
18
24
  """
19
- pass
25
+ The main check function to compare a the methods within a backend model to a corresponding TypeScript file.
20
26
 
27
+ Notes
28
+ -----
29
+ The available command line arguments are:
30
+ - --backend-model-file (-bmf): Path to the backend model file (e.g. Python class)
31
+ - --typescript-file (-tsf): Path to the TypeScript interface/type file
21
32
 
22
- @cli.command()
23
- @click.argument("backend_model", type=click.Path(exists=True))
24
- @click.argument("typescript_file", type=click.Path(exists=True))
25
- def check(backend_model: str, typescript_file: str):
33
+ Examples
34
+ --------
35
+ >>> ts-backend-check -bmf <backend-model-file> -tsf <typescript-file>
26
36
  """
27
- Check TypeScript types against backend models.
37
+ # MARK: CLI Base
28
38
 
29
- This command checks if all fields from the backend model are properly represented
30
- in the TypeScript types file. It supports marking fields as backend-only using
31
- special comments in the TypeScript file.
39
+ parser = ArgumentParser(
40
+ prog="ts-backend-check",
41
+ description="Checks the types in TypeScript files against the corresponding backend models.",
42
+ epilog="Visit the codebase at https://github.com/activist-org/ts-backend-check to learn more!",
43
+ formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=60),
44
+ )
32
45
 
33
- Parameters
34
- ----------
35
- backend_model : str
36
- The path to the backend model file (e.g. Python class).
46
+ parser._actions[0].help = "Show this help message and exit."
37
47
 
38
- typescript_file : str
39
- The path to the TypeScript interface/type file.
48
+ parser.add_argument(
49
+ "-v",
50
+ "--version",
51
+ action="version",
52
+ version=f"{get_version_message()}",
53
+ help="Show the version of the ts-backend-check CLI.",
54
+ )
40
55
 
41
- Examples
42
- --------
43
- ts-backend-check check src/models/user.py src/types/user.ts
44
- """
45
- checker = TypeChecker(models_file=backend_model, types_file=typescript_file)
46
- if missing := checker.check():
47
- click.echo("Missing TypeScript fields found:")
48
- click.echo("\n".join(missing))
49
- click.echo(
50
- f"\nPlease correct the {len(missing)} fields above to have backend models and frontend types fully synced."
56
+ parser.add_argument(
57
+ "-u",
58
+ "--upgrade",
59
+ action="store_true",
60
+ help="Upgrade the ts-backend-check CLI to the latest version.",
61
+ )
62
+
63
+ parser.add_argument(
64
+ "-bmf",
65
+ "--backend-model-file",
66
+ help="Path to the backend model file (e.g. Python class).",
67
+ )
68
+
69
+ parser.add_argument(
70
+ "-tsf",
71
+ "--typescript-file",
72
+ help="Path to the TypeScript interface/type file.",
73
+ )
74
+
75
+ parser.add_argument(
76
+ "-c",
77
+ "--configure",
78
+ action="store_true",
79
+ help="Configure a YAML file to simplify your checks.",
80
+ )
81
+
82
+ parser.add_argument(
83
+ "-cb",
84
+ "--check-blank",
85
+ help="Check for fields marked blank=True within Django models.",
86
+ )
87
+
88
+ # MARK: Setup CLI
89
+
90
+ args = parser.parse_args(args=None if sys.argv[1:] else ["--help"])
91
+
92
+ if args.upgrade:
93
+ upgrade_cli()
94
+ return
95
+
96
+ if args.configure:
97
+ create_config()
98
+ return
99
+
100
+ if args.check_blank:
101
+ check_blank(args.check_blank)
102
+ return
103
+
104
+ # MARK: Run Check
105
+
106
+ backend_model_file_path = ROOT_DIR / args.backend_model_file
107
+ ts_file_path = ROOT_DIR / args.typescript_file
108
+
109
+ if not backend_model_file_path.is_file():
110
+ rprint(
111
+ f"[red]{args.backend_model_file} that should contain the backend models does not exist. Please check and try again.[/red]"
112
+ )
113
+
114
+ elif not ts_file_path.is_file():
115
+ rprint(
116
+ f"[red]{args.typescript_file} file that should contain the TypeScript types does not exist. Please check and try again.[/red]"
51
117
  )
52
- sys.exit(1)
53
118
 
54
- click.echo("All model fields are properly typed in TypeScript!")
119
+ else:
120
+ checker = TypeChecker(
121
+ models_file=args.backend_model_file,
122
+ types_file=args.typescript_file,
123
+ )
124
+
125
+ if missing := checker.check():
126
+ rprint(
127
+ "\n[red]❌ ts-backend-check error: Missing typescript fields found:[/red]\n"
128
+ )
129
+
130
+ # Print each error message in red.
131
+ for msg in missing:
132
+ rprint(Text.from_markup(f"[red]{msg}[/red]"))
133
+
134
+ field_or_fields = "fields" if len(missing) > 1 else "field"
135
+ rprint(
136
+ f"\n[red]Please fix the {len(missing)} {field_or_fields} above to have the backend models synced with the typescript interfaces.[/red]"
137
+ )
138
+ sys.exit(1)
139
+
140
+ rprint(
141
+ "[green]✅ Success: All models are synced with their corresponding TypeScript interfaces.[/green]"
142
+ )
55
143
 
56
144
 
57
145
  if __name__ == "__main__":
58
- cli()
146
+ main()
@@ -0,0 +1,91 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """
3
+ Functions to update the ts-backend-check CLI based on install method.
4
+ """
5
+
6
+ import subprocess
7
+ import sys
8
+
9
+ from packaging import version
10
+ from packaging.version import InvalidVersion
11
+
12
+ from ts_backend_check.cli.version import (
13
+ UNKNOWN_VERSION_NOT_FETCHED,
14
+ get_latest_version,
15
+ get_local_version,
16
+ )
17
+
18
+
19
+ def upgrade_cli() -> None:
20
+ """
21
+ Upgrade the CLI tool to the latest available version on PyPI.
22
+
23
+ Raises
24
+ ------
25
+ subprocess.CalledProcessError
26
+ If the installation of the latest version fails.
27
+ """
28
+ local_version = get_local_version()
29
+ latest_version_message = get_latest_version()
30
+
31
+ if latest_version_message == UNKNOWN_VERSION_NOT_FETCHED:
32
+ print(
33
+ "Unable to fetch the latest version from GitHub. Please check the GitHub repository or your internet connection."
34
+ )
35
+ return
36
+
37
+ latest_version = latest_version_message.split("v")[-1]
38
+ local_version_clean = local_version.strip()
39
+ latest_version_clean = latest_version.replace("ts-backend-check", "").strip()
40
+
41
+ # Handle empty or invalid version strings.
42
+ try:
43
+ local_ver = (
44
+ version.parse(local_version_clean)
45
+ if local_version_clean
46
+ else version.parse("0.0.0")
47
+ )
48
+
49
+ except InvalidVersion:
50
+ # If local version is invalid, treat it as 0.0.0 to force upgrade.
51
+ local_ver = version.parse("0.0.0")
52
+
53
+ try:
54
+ latest_ver = version.parse(latest_version_clean)
55
+
56
+ except InvalidVersion:
57
+ print("Unable to parse the latest version. Please check the GitHub repository.")
58
+ return
59
+
60
+ if local_ver == latest_ver:
61
+ print("You already have the latest version of ts-backend-check.")
62
+
63
+ elif local_ver > latest_ver:
64
+ print(
65
+ f"ts-backend-check v{local_version_clean} is higher than the currently released version ts-backend-check v{latest_version_clean}. Hopefully this is a development build, and if so, thanks for your work on ts-backend-check! If not, please report this to the team at https://github.com/activist-org/ts-backend-check/issues."
66
+ )
67
+
68
+ else:
69
+ print(f"Current version: {local_version}")
70
+ print(f"Latest version: {latest_version}")
71
+ print("Updating ts-backend-check with pip...")
72
+ try:
73
+ subprocess.check_call(
74
+ [
75
+ sys.executable,
76
+ "-m",
77
+ "pip",
78
+ "install",
79
+ "--upgrade",
80
+ "ts-backend-check",
81
+ ]
82
+ )
83
+
84
+ except subprocess.CalledProcessError as e:
85
+ print(
86
+ f"Failed to install the latest version of ts-backend-check with error {e}. Please check the error message and report any issues to the team at https://github.com/activist-org/ts-backend-check/issues."
87
+ )
88
+
89
+
90
+ if __name__ == "__main__":
91
+ upgrade_cli()
@@ -0,0 +1,87 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """
3
+ Functions for checking current version of the ts-backend-check CLI.
4
+ """
5
+
6
+ import importlib.metadata
7
+ from typing import Any, Dict
8
+
9
+ import requests
10
+
11
+ UNKNOWN_VERSION = "Unknown ts-backend-check version"
12
+ UNKNOWN_VERSION_NOT_PIP = f"{UNKNOWN_VERSION} (Not installed via pip)"
13
+ UNKNOWN_VERSION_NOT_FETCHED = f"{UNKNOWN_VERSION} (Unable to fetch version)"
14
+
15
+
16
+ def get_local_version() -> str:
17
+ """
18
+ Get the local version of the ts-backend-check package.
19
+
20
+ Returns
21
+ -------
22
+ str
23
+ The version of the installed ts-backend-check package, or a message indicating
24
+ that the package is not installed via pip.
25
+ """
26
+ try:
27
+ return importlib.metadata.version("ts-backend-check")
28
+
29
+ except importlib.metadata.PackageNotFoundError:
30
+ return UNKNOWN_VERSION_NOT_PIP
31
+
32
+
33
+ def get_latest_version() -> Any:
34
+ """
35
+ Get the latest version of the ts-backend-check package from GitHub.
36
+
37
+ Returns
38
+ -------
39
+ Any
40
+ The latest version of the ts-backend-check package, or a message indicating
41
+ that the version could not be fetched.
42
+ """
43
+ try:
44
+ response = requests.get(
45
+ "https://api.github.com/repos/activist-org/ts-backend-check/releases/latest"
46
+ )
47
+ response_data: Dict[str, Any] = response.json()
48
+ return response_data["name"]
49
+
50
+ except Exception:
51
+ return UNKNOWN_VERSION_NOT_FETCHED
52
+
53
+
54
+ def get_version_message() -> str:
55
+ """
56
+ Get a message indicating the local and latest versions of the ts-backend-check package.
57
+
58
+ Returns
59
+ -------
60
+ str
61
+ A message indicating the local version, the latest version, and whether
62
+ an upgrade is available.
63
+ """
64
+ local_version = get_local_version()
65
+ latest_version = get_latest_version()
66
+
67
+ if local_version == UNKNOWN_VERSION_NOT_PIP:
68
+ return UNKNOWN_VERSION_NOT_PIP
69
+
70
+ elif latest_version == UNKNOWN_VERSION_NOT_FETCHED:
71
+ return UNKNOWN_VERSION_NOT_FETCHED
72
+
73
+ local_version_clean = local_version.strip()
74
+ latest_version_clean = latest_version.replace("ts-backend-check", "").strip()
75
+
76
+ if local_version_clean == latest_version_clean:
77
+ return f"ts-backend-check v{local_version_clean}"
78
+
79
+ elif local_version_clean > latest_version_clean:
80
+ return f"ts-backend-check v{local_version_clean} is higher than the currently released version ts-backend-check v{latest_version_clean}. Hopefully this is a development build, and if so, thanks for your work on ts-backend-check! If not, please report this to the team at https://github.com/activist-org/ts-backend-check/issues."
81
+
82
+ else:
83
+ return f"ts-backend-check v{local_version_clean} (Upgrade available: ts-backend-check v{latest_version_clean}). To upgrade: ts-backend-check -u"
84
+
85
+
86
+ if __name__ == "__main__":
87
+ print(get_version_message())
File without changes
@@ -0,0 +1,113 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """
3
+ Module for parsing Django models and extracting field information.
4
+ """
5
+
6
+ import ast
7
+ from typing import Dict, Set
8
+
9
+
10
+ class DjangoModelVisitor(ast.NodeVisitor):
11
+ """
12
+ AST visitor to extract fields from Django models.
13
+ """
14
+
15
+ DJANGO_FIELD_TYPES = {
16
+ "Field",
17
+ "CharField",
18
+ "TextField",
19
+ "IntegerField",
20
+ "BooleanField",
21
+ "DateTimeField",
22
+ "ForeignKey",
23
+ "ManyToManyField",
24
+ "OneToOneField",
25
+ "EmailField",
26
+ "URLField",
27
+ "FileField",
28
+ "ImageField",
29
+ "DecimalField",
30
+ "AutoField",
31
+ }
32
+
33
+ def __init__(self) -> None:
34
+ self.models: Dict[str, Set[str]] = {}
35
+ self.current_model: str | None = None
36
+
37
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
38
+ """
39
+ Check class definitions, specifically those that inherit from other classes.
40
+
41
+ Parameters
42
+ ----------
43
+ node : ast.ClassDef
44
+ A class definition from Python AST (Abstract Syntax Tree).
45
+ It contains information about the class, such as its name, base classes, body, decorators, etc.
46
+ """
47
+ # Only process classes that inherit from something.
48
+ if node.bases:
49
+ self.current_model = node.name
50
+ if self.current_model not in self.models:
51
+ self.models[self.current_model] = set()
52
+
53
+ self.generic_visit(node)
54
+
55
+ self.current_model = None
56
+
57
+ def visit_Assign(self, node: ast.Assign) -> None:
58
+ """
59
+ Check assignment statements within a class.
60
+
61
+ Parameters
62
+ ----------
63
+ node : ast.Assign
64
+ An assignment definition from Python AST (Abstract Syntax Tree).
65
+ It represents an assignment statement (e.g., x = 42).
66
+ """
67
+ if not self.current_model:
68
+ return
69
+
70
+ for target in node.targets:
71
+ if (
72
+ isinstance(target, ast.Name)
73
+ and not target.id.startswith("_")
74
+ and isinstance(node.value, ast.Call)
75
+ and hasattr(node.value.func, "attr")
76
+ ) and any(
77
+ field_type in node.value.func.attr
78
+ for field_type in self.DJANGO_FIELD_TYPES
79
+ ):
80
+ self.models[self.current_model].add(target.id)
81
+
82
+
83
+ def extract_model_fields(models_file: str) -> Dict[str, Set[str]]:
84
+ """
85
+ Extract fields from Django models file.
86
+
87
+ Parameters
88
+ ----------
89
+ models_file : str
90
+ A models.py file that defines Django models.
91
+
92
+ Returns
93
+ -------
94
+ Dict[str, Set[str]]
95
+ The fields from the models file extracted into a dictionary for future processing.
96
+ """
97
+ with open(models_file, "r", encoding="utf-8") as f:
98
+ content = f.read().strip()
99
+ # Skip any empty lines at the beginning.
100
+ while content.startswith("\n"):
101
+ content = content[1:]
102
+
103
+ try:
104
+ tree = ast.parse(content)
105
+ except SyntaxError as e:
106
+ raise SyntaxError(
107
+ f"Failed to parse {models_file}. Make sure it's a valid Python file. Error: {str(e)}"
108
+ ) from e
109
+
110
+ visitor = DjangoModelVisitor()
111
+ visitor.visit(tree)
112
+
113
+ return visitor.models
@@ -0,0 +1,105 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """
3
+ Module for parsing TypeScript interfaces and types.
4
+ """
5
+
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Dict, List, Set
9
+
10
+
11
+ @dataclass
12
+ class TypeScriptInterface:
13
+ """
14
+ Represents a TypeScript interface with its fields and parent interfaces.
15
+ """
16
+
17
+ name: str
18
+ fields: Set[str]
19
+ parents: List[str]
20
+
21
+
22
+ class TypeScriptParser:
23
+ """
24
+ Parser for TypeScript interface files.
25
+
26
+ Parameters
27
+ ----------
28
+ file_path : str
29
+ The file path for the TypeScript file to parse.
30
+ """
31
+
32
+ def __init__(self, file_path: str) -> None:
33
+ self.file_path = file_path
34
+ with open(file_path, "r", encoding="utf-8") as f:
35
+ self.content = f.read()
36
+
37
+ def parse_interfaces(self) -> Dict[str, TypeScriptInterface]:
38
+ """
39
+ Parse TypeScript interfaces from the file.
40
+
41
+ Returns
42
+ -------
43
+ Dict[str, TypeScriptInterface]
44
+ The interface parsed into a dictionary for future processing.
45
+ """
46
+ interfaces = {}
47
+ interface_pattern = (
48
+ r"(?:export\s+|declare\s+)?interface\s+(\w+)"
49
+ r"(?:\s+extends\s+([^{]+))?\s*{([\s\S]*?)}"
50
+ )
51
+
52
+ for match in re.finditer(interface_pattern, self.content):
53
+ name = match.group(1)
54
+ parents = (
55
+ [p.strip() for p in match.group(2).split(",")] if match.group(2) else []
56
+ )
57
+ fields = self._extract_fields(match.group(3))
58
+
59
+ interfaces[name] = TypeScriptInterface(name, fields, parents)
60
+
61
+ return interfaces
62
+
63
+ def get_backend_only_fields(self) -> Set[str]:
64
+ """
65
+ Extract fields marked as backend-only in comments.
66
+
67
+ Returns
68
+ -------
69
+ Set[str]
70
+ The field names that are marked with a backend-only identifier to ignore them.
71
+ """
72
+ patterns = [
73
+ r"//.*?Note:\s*(\w+)\s+is\s+backend\s+only",
74
+ r"//.*?(\w+)\s+is\s+backend\s+only",
75
+ r"//\s*@backend-only\s+(\w+)",
76
+ r"//.*?backend-only:\s*(\w+)",
77
+ ]
78
+ return {
79
+ match for pattern in patterns for match in re.findall(pattern, self.content)
80
+ }
81
+
82
+ @staticmethod
83
+ def _extract_fields(interface_body: str) -> Set[str]:
84
+ """
85
+ Extract field names from interface body.
86
+
87
+ Parameters
88
+ ----------
89
+ interface_body : str
90
+ A string representation of the interface body of the model.
91
+
92
+ Returns
93
+ -------
94
+ Set[str]
95
+ The field names from the model interface body.
96
+ """
97
+ fields = set()
98
+
99
+ # Regular fields
100
+ fields.update(re.findall(r"(?://[^\n]*\n)*\s*(\w+)\s*[?]?\s*:", interface_body))
101
+
102
+ # Readonly fields
103
+ fields.update(re.findall(r"readonly\s+(\w+)\s*[?]?\s*:", interface_body))
104
+
105
+ return fields
ts_backend_check/utils.py CHANGED
@@ -6,7 +6,7 @@ Utility functions for ts-backend-check.
6
6
 
7
7
  def snake_to_camel(input_str: str) -> str:
8
8
  """
9
- Convert snake_case to camelCase.
9
+ Convert snake_case to camelCase while preserving existing camelCase components.
10
10
 
11
11
  Parameters
12
12
  ----------
@@ -17,6 +17,23 @@ def snake_to_camel(input_str: str) -> str:
17
17
  -------
18
18
  str
19
19
  The camelCase version of the input string.
20
+
21
+ Examples
22
+ --------
23
+ hello_world -> helloWorld, alreadyCamelCase -> alreadyCamelCase
20
24
  """
21
- components = input_str.split("_")
22
- return components[0] + "".join(x.title() for x in components[1:])
25
+ if not input_str or input_str.startswith("_"):
26
+ return input_str
27
+
28
+ words = input_str.split("_")
29
+ result = words[0].lower()
30
+
31
+ for word in words[1:]:
32
+ if word:
33
+ if any(c.isupper() for c in word[1:]):
34
+ result += word[0].upper() + word[1:]
35
+
36
+ else:
37
+ result += word[0].upper() + word[1:].lower()
38
+
39
+ return result
@@ -0,0 +1,26 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.13
3
+ # by the following command:
4
+ #
5
+ # pip-compile requirements.in
6
+ #
7
+ certifi>=2025.11.12
8
+ # via requests
9
+ charset-normalizer>=3.4.4
10
+ # via requests
11
+ idna>=3.11
12
+ # via requests
13
+ markdown-it-py>=4.0.0
14
+ # via rich
15
+ mdurl>=0.1.2
16
+ # via markdown-it-py
17
+ pygments>=2.19.2
18
+ # via rich
19
+ pyyaml>=6.0.3
20
+ # via -r requirements.in
21
+ requests>=2.32.5
22
+ # via -r requirements.in
23
+ rich>=14.2.0
24
+ # via -r requirements.in
25
+ urllib3>=2.6.2
26
+ # via requests
@@ -1,25 +1,54 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ts-backend-check
3
- Version: 1.0.0
4
- Summary: ts-backend-check is a Python package used to check TypeScript types against their corresponding backend models to assure that all fields have been accounted for.
3
+ Version: 1.2.0
4
+ Summary: Check TypeScript types against their corresponding backend models to assure that all fields have been accounted for.
5
5
  Home-page: https://github.com/activist-org/ts-backend-check
6
6
  Author: ts-backend-check developers
7
- Author-email: andrew.t.mcallister@gmail.com
8
- Requires-Python: >=3.8
7
+ Author-email: team@activist.org
8
+ License: : OSI Approved :: GNU License
9
+ Keywords: backend,typescript,validation,ci,cli
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Operating System :: OS Independent
21
+ Requires-Python: >=3.10
9
22
  Description-Content-Type: text/markdown
10
23
  License-File: LICENSE.txt
11
- Requires-Dist: click>=8.1
24
+ Requires-Dist: certifi>=2025.11.12
25
+ Requires-Dist: charset-normalizer>=3.4.4
26
+ Requires-Dist: idna>=3.11
27
+ Requires-Dist: markdown-it-py>=4.0.0
28
+ Requires-Dist: mdurl>=0.1.2
29
+ Requires-Dist: pygments>=2.19.2
30
+ Requires-Dist: pyyaml>=6.0.3
31
+ Requires-Dist: requests>=2.32.5
32
+ Requires-Dist: rich>=14.2.0
33
+ Requires-Dist: urllib3>=2.6.2
34
+ Dynamic: author
12
35
  Dynamic: author-email
36
+ Dynamic: classifier
37
+ Dynamic: description
38
+ Dynamic: description-content-type
13
39
  Dynamic: home-page
14
40
  Dynamic: license-file
41
+ Dynamic: requires-dist
15
42
  Dynamic: requires-python
43
+ Dynamic: summary
16
44
 
17
45
  <div align="center">
18
- <a href="https://github.com/activist-org/ts-backend-check"><img src="https://raw.githubusercontent.com/activist-org/ts-backend-check/main/.github/resources/TSBECheckGitHubBanner.png" width=1024 alt="TSBE Check logo"></a>
46
+ <a href="https://github.com/activist-org/ts-backend-check"><img src="https://raw.githubusercontent.com/activist-org/ts-backend-check/main/.github/resources/TSBackendCheckGitHubBanner.png" width=1024 alt="TS Backend Check logo"></a>
19
47
  </div>
20
48
 
21
49
  [![rtd](https://img.shields.io/readthedocs/ts-backend-check.svg?label=%20&logo=read-the-docs&logoColor=ffffff)](http://ts-backend-check.readthedocs.io/en/latest/)
22
- [![ci_backend](https://img.shields.io/github/actions/workflow/status/activist-org/ts-backend-check/pr_ci.yaml?branch=main&label=%20&logo=pytest&logoColor=ffffff)](https://github.com/activist-org/ts-backend-check/actions/workflows/pr_ci_backend.yaml)
50
+ [![pr_ci](https://img.shields.io/github/actions/workflow/status/activist-org/ts-backend-check/pr_ci.yaml?branch=main&label=%20&logo=ruff&logoColor=ffffff)](https://github.com/activist-org/ts-backend-check/actions/workflows/pr_ci.yaml)
51
+ [![python_package_ci](https://img.shields.io/github/actions/workflow/status/activist-org/ts-backend-check/python_package_ci.yaml?branch=main&label=%20&logo=pytest&logoColor=ffffff)](https://github.com/activist-org/ts-backend-check/actions/workflows/python_package_ci.yaml)
23
52
  [![issues](https://img.shields.io/github/issues/activist-org/ts-backend-check?label=%20&logo=github)](https://github.com/activist-org/ts-backend-check/issues)
24
53
  [![python](https://img.shields.io/badge/Python-4B8BBE.svg?logo=python&logoColor=ffffff)](https://github.com/activist-org/ts-backend-check/blob/main/CONTRIBUTING.md)
25
54
  [![pypi](https://img.shields.io/pypi/v/ts-backend-check.svg?label=%20&color=4B8BBE)](https://pypi.org/project/ts-backend-check/)
@@ -38,10 +67,10 @@ Dynamic: requires-python
38
67
 
39
68
  - [Usage](#usage-)
40
69
  - [Contributing](#contributing-)
41
- - [Environment setup](#environment-setup)
70
+ - [Environment setup](#environment-setup-)
42
71
  - [Contributors](#contributors-)
43
72
 
44
- <a id="usage"></a>
73
+ <a id="usage-"></a>
45
74
 
46
75
  ## Usage [`⇧`](#contents)
47
76
 
@@ -51,6 +80,11 @@ Dynamic: requires-python
51
80
  pip install ts-backend-check
52
81
  ```
53
82
 
83
+ ### Command Options
84
+
85
+ - `backend-model-file` (`bmf`): Path to the backend model file (e.g. Python class)
86
+ - `typescript-file` (`tsf`): Path to the TypeScript interface/type file
87
+
54
88
  ### Basic Usage
55
89
 
56
90
  The CLI provides a simple interface to check TypeScript types against backend models:
@@ -60,31 +94,19 @@ The CLI provides a simple interface to check TypeScript types against backend mo
60
94
  ts-backend-check --help
61
95
 
62
96
  # Check a TypeScript type against a backend model:
63
- ts-backend-check check <typescript_file> <backend_model>
97
+ ts-backend-check -bmf <backend-model-file> -tsf <typescript-file>
64
98
 
65
99
  # Example command:
66
- ts-backend-check check src/types/user.ts src/models/user.py
67
- ```
68
-
69
- ### Command Options
70
-
71
- - `check`: Compare TypeScript types with backend models
72
- - `typescript_file`: Path to the TypeScript interface/type file
73
- - `backend_model`: Path to the backend model file (e.g. Python class)
74
-
75
- ### Version Information
76
-
77
- ```bash
78
- ts-backend-check --version
100
+ ts-backend-check -bmf src/models/user.py -tsf src/types/user.ts
79
101
  ```
80
102
 
81
- <a id="contributing"></a>
103
+ <a id="contributing-"></a>
82
104
 
83
105
  # Contributing [`⇧`](#contents)
84
106
 
85
- <a href="https://matrix.to/#/#activist_community:matrix.org"><img src="https://raw.githubusercontent.com/activist-org/Organization/main/resources/images/logos/MatrixLogoGrey.png" height="50" alt="Public Matrix Chat" align="right"></a>
107
+ <a href="https://matrix.to/#/#activist_community:matrix.org"><img src="https://raw.githubusercontent.com/activist-org/Organization/main/resources/images/logos/MatrixLogoGrey.png" width="175" alt="Public Matrix Chat" align="right"></a>
86
108
 
87
- activist uses [Matrix](https://matrix.org/) for internal communication. You're more than welcome to [join us in our public chat rooms](https://matrix.to/#/#activist_community:matrix.org) to share ideas, ask questions or just say hi to the team :)
109
+ activist uses [Matrix](https://matrix.org/) for internal communication. You're more than welcome to [join us in our public chat rooms](https://matrix.to/#/#activist_community:matrix.org) to share ideas, ask questions or just say hi to the team :) We'd suggest that you use the [Element](https://element.io/) client and [Element X](https://element.io/app) for a mobile app.
88
110
 
89
111
  Please see the [contribution guidelines](CONTRIBUTING.md) if you are interested in contributing. Work that is in progress or could be implemented is tracked in the [issues](https://github.com/activist-org/ts-backend-check/issues) and [projects](https://github.com/activist-org/ts-backend-check/projects).
90
112
 
@@ -95,7 +117,7 @@ Also check the [`-next release-`](https://github.com/activist-org/ts-backend-che
95
117
 
96
118
  We would be happy to discuss granting you further rights as a contributor after your first pull requests, with a maintainer role then being possible after continued interest in the project. activist seeks to be an inclusive, diverse and supportive organization. We'd love to have you on the team!
97
119
 
98
- <a id="how-you-can-help"></a>
120
+ <a id="how-you-can-help-"></a>
99
121
 
100
122
  ## How you can help [`⇧`](#contents)
101
123
 
@@ -103,7 +125,7 @@ We would be happy to discuss granting you further rights as a contributor after
103
125
  - Working with us on [new features](https://github.com/activist-org/ts-backend-check/issues?q=is%3Aissue+is%3Aopen+label%3Afeature) ✨
104
126
  - [Documentation](https://github.com/activist-org/ts-backend-check/issues?q=is%3Aissue+is%3Aopen+label%3Adocumentation) for onboarding and project cohesion 📝
105
127
 
106
- <a id="environment-setup"></a>
128
+ <a id="environment-setup-"></a>
107
129
 
108
130
  # Environment setup [`⇧`](#contents)
109
131
 
@@ -0,0 +1,21 @@
1
+ ts_backend_check/__init__.py,sha256=HSiEqWvU3Q4a6D7tYsoq3-B-g1HdUOGngEeGV45j8ak,172
2
+ ts_backend_check/checker.py,sha256=ISPl_1H7dXb7DcOl6erdxEhx61xQBJxtHuYkUuEqrck,5792
3
+ ts_backend_check/django_parser.py,sha256=CFg4-zV5BsVhuq-nVQEYD9nS25JSaHbIs2Xr3LE3KWY,3241
4
+ ts_backend_check/typescript_parser.py,sha256=BqBGOjsf-Wlh6ceaK-G2vLm5AMiEiwX-XnJQrr8P-gQ,2885
5
+ ts_backend_check/utils.py,sha256=N9_25_wW2g3dkmGGLWHQj_AHsXI9Rx2XqRnYxT5i2Rk,897
6
+ ts_backend_check/cli/__init__.py,sha256=wJK9tO9MtI10L0xRjrk7WP_qZZRBjbFw1U9jJbIbX7I,108
7
+ ts_backend_check/cli/check_blank.py,sha256=gg6v01UYNmzs8eLasxauCaBCfN9esUtiAwqhqNTzE3w,3377
8
+ ts_backend_check/cli/config.py,sha256=AgzjY3qbgW4ya5JJEdKaBQ9J8h0Ip-39xJNbAjFpA6s,4463
9
+ ts_backend_check/cli/main.py,sha256=6g9Rf_m5H2cWUoPYPU_HEmkoUl6SLLai1Ni2BpgDz6U,4222
10
+ ts_backend_check/cli/upgrade.py,sha256=P2LdIDCLdOFs-fOMuKLQoUMijQIyKEIA5PFZVsAUwDg,2952
11
+ ts_backend_check/cli/version.py,sha256=lsocMaxAfF-FHW9O-NGH1sAo6svjuLgdGFxaC-XVoZc,2845
12
+ ts_backend_check/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ ts_backend_check/parsers/django_parser.py,sha256=U5OP6IyrSdwI9rDUGU4GbWTDfozwhretWg7lmF6QGB8,3242
14
+ ts_backend_check/parsers/typescript_parser.py,sha256=BqBGOjsf-Wlh6ceaK-G2vLm5AMiEiwX-XnJQrr8P-gQ,2885
15
+ ts_backend_check-1.2.0.data/data/requirements.txt,sha256=ixdwNPm86WRSX0oBBaNegCQL7HQP8evsCRAdEFHLxGk,540
16
+ ts_backend_check-1.2.0.dist-info/licenses/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
17
+ ts_backend_check-1.2.0.dist-info/METADATA,sha256=FDqxUd_nslUpUNEtk5sAHxEkATcDVkUAJkBMVYkf0rA,10416
18
+ ts_backend_check-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ ts_backend_check-1.2.0.dist-info/entry_points.txt,sha256=QeY7RJu20otBnQlhMMZxPL8nB3mP3M3U3h3nOlSFWSU,68
20
+ ts_backend_check-1.2.0.dist-info/top_level.txt,sha256=VzfNWQ3fPNdl-OBdtUKsUWR8Asdd27E3OJNUg2oQU8Y,17
21
+ ts_backend_check-1.2.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ts-backend-check = ts_backend_check.cli.main:main
@@ -1,6 +0,0 @@
1
- #
2
- # This file is autogenerated by pip-compile with Python 3.13
3
- # by the following command:
4
- #
5
- # pip-compile requirements.in
6
- #
@@ -1,14 +0,0 @@
1
- ts_backend_check/__init__.py,sha256=5Y_YDmaFl9cjVy1GqGeml-QaPUpcJyWgjCYe_vJoHvw,156
2
- ts_backend_check/checker.py,sha256=8mLtLBt6ok2cDgt-3u8yb3hClf-3ojU8zIsqL2z_GdQ,5727
3
- ts_backend_check/django_parser.py,sha256=CFg4-zV5BsVhuq-nVQEYD9nS25JSaHbIs2Xr3LE3KWY,3241
4
- ts_backend_check/typescript_parser.py,sha256=BqBGOjsf-Wlh6ceaK-G2vLm5AMiEiwX-XnJQrr8P-gQ,2885
5
- ts_backend_check/utils.py,sha256=Kfa6ZvjnsGrGJJX18FhfzmQtjMrjPwfECZiZL4zgGRQ,475
6
- ts_backend_check/cli/__init__.py,sha256=bxThVZZgFRXoWgZ4pasljY6ATZPTJVh_KeBebweDJb0,129
7
- ts_backend_check/cli/main.py,sha256=RUJsonWBUnCqg93ayuDo0LJYhCLKuVb9V9Ycx5cbts4,1636
8
- ts_backend_check-1.0.0.data/data/requirements.txt,sha256=0FbFr7FTG31FEs_FMsN3MrAIFJIeGdDC6POOOb2FeA0,134
9
- ts_backend_check-1.0.0.dist-info/licenses/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
10
- ts_backend_check-1.0.0.dist-info/METADATA,sha256=1hA_CrBvSqePiw3G0ZBAvm5kEmGBRSYHGlUOmsagJnw,9084
11
- ts_backend_check-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- ts_backend_check-1.0.0.dist-info/entry_points.txt,sha256=6_OttJoqgUr1BZYjy6Rcx5g0iYcwJD2o1c5q_6OcXzw,58
13
- ts_backend_check-1.0.0.dist-info/top_level.txt,sha256=VzfNWQ3fPNdl-OBdtUKsUWR8Asdd27E3OJNUg2oQU8Y,17
14
- ts_backend_check-1.0.0.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- ts-backend-check = ts_backend_check:cli