python-color-math 0.2.0__tar.gz → 0.2.1__tar.gz

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.
Files changed (64) hide show
  1. {python_color_math-0.2.0/python_color_math.egg-info → python_color_math-0.2.1}/PKG-INFO +4 -2
  2. {python_color_math-0.2.0 → python_color_math-0.2.1}/README.md +1 -1
  3. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/__init__.py +1 -1
  4. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/config.py +56 -4
  5. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/block.py +15 -5
  6. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/derivative.py +3 -3
  7. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/generic.py +34 -12
  8. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/matrix.py +5 -3
  9. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/main.py +16 -1
  10. python_color_math-0.2.1/color_math/mcp.py +42 -0
  11. python_color_math-0.2.1/color_math/mcp_server.py +425 -0
  12. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/braket.py +20 -21
  13. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/differentials.py +12 -9
  14. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/dimensionless.py +7 -8
  15. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/latex_spans.py +6 -13
  16. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/markdown_scanner.py +50 -3
  17. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/math_parser.py +18 -17
  18. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/scanner.py +2 -0
  19. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/taxonomy.py +11 -16
  20. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/units.py +32 -30
  21. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/variable_hash.py +13 -19
  22. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/undo.py +15 -7
  23. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/utils/latex_helpers.py +89 -8
  24. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/utils/spans.py +61 -5
  25. {python_color_math-0.2.0 → python_color_math-0.2.1}/pyproject.toml +5 -1
  26. {python_color_math-0.2.0 → python_color_math-0.2.1/python_color_math.egg-info}/PKG-INFO +4 -2
  27. {python_color_math-0.2.0 → python_color_math-0.2.1}/python_color_math.egg-info/SOURCES.txt +8 -1
  28. {python_color_math-0.2.0 → python_color_math-0.2.1}/python_color_math.egg-info/entry_points.txt +1 -0
  29. python_color_math-0.2.1/python_color_math.egg-info/requires.txt +3 -0
  30. python_color_math-0.2.1/tests/test_adversarial.py +207 -0
  31. {python_color_math-0.2.0 → python_color_math-0.2.1}/tests/test_features.py +59 -0
  32. python_color_math-0.2.1/tests/test_fuzz.py +218 -0
  33. python_color_math-0.2.1/tests/test_mcp_hardcore.py +411 -0
  34. python_color_math-0.2.1/tests/test_mcp_server.py +116 -0
  35. {python_color_math-0.2.0 → python_color_math-0.2.1}/.pre-commit-hooks.yaml +0 -0
  36. {python_color_math-0.2.0 → python_color_math-0.2.1}/LICENSE +0 -0
  37. {python_color_math-0.2.0 → python_color_math-0.2.1}/MANIFEST.in +0 -0
  38. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/__main__.py +0 -0
  39. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/adapters.py +0 -0
  40. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/completions.py +0 -0
  41. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/__init__.py +0 -0
  42. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/align.py +0 -0
  43. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/equation.py +0 -0
  44. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/integral.py +0 -0
  45. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/limit.py +0 -0
  46. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/converters/semantic.py +0 -0
  47. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/gui.py +0 -0
  48. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/io.py +0 -0
  49. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/__init__.py +0 -0
  50. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/alignment.py +0 -0
  51. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/constants.py +0 -0
  52. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/parsers/delimiters.py +0 -0
  53. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/py.typed +0 -0
  54. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/self_test.py +0 -0
  55. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/tutorial.py +0 -0
  56. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/utils/__init__.py +0 -0
  57. {python_color_math-0.2.0 → python_color_math-0.2.1}/color_math/utils/coloring.py +0 -0
  58. {python_color_math-0.2.0 → python_color_math-0.2.1}/python_color_math/__init__.py +0 -0
  59. {python_color_math-0.2.0 → python_color_math-0.2.1}/python_color_math/__main__.py +0 -0
  60. {python_color_math-0.2.0 → python_color_math-0.2.1}/python_color_math.egg-info/dependency_links.txt +0 -0
  61. {python_color_math-0.2.0 → python_color_math-0.2.1}/python_color_math.egg-info/top_level.txt +0 -0
  62. {python_color_math-0.2.0 → python_color_math-0.2.1}/setup.cfg +0 -0
  63. {python_color_math-0.2.0 → python_color_math-0.2.1}/tests/test_cli.py +0 -0
  64. {python_color_math-0.2.0 → python_color_math-0.2.1}/tests/test_gui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-color-math
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A fast, local semantic LaTeX colorizer for Markdown, Jupyter, and LaTeX math equations.
5
5
  Author: Aditya
6
6
  License: MIT License
@@ -51,6 +51,8 @@ Classifier: Topic :: Text Processing :: Markup :: Markdown
51
51
  Requires-Python: >=3.10
52
52
  Description-Content-Type: text/markdown
53
53
  License-File: LICENSE
54
+ Provides-Extra: mcp
55
+ Requires-Dist: mcp>=1.0.0; extra == "mcp"
54
56
  Dynamic: license-file
55
57
 
56
58
  # 🎨 Python Color Math
@@ -230,7 +232,7 @@ Integrate `color-math` into your git workflow using [pre-commit](https://pre-com
230
232
  ```yaml
231
233
  repos:
232
234
  - repo: https://github.com/36ty-blip/color-math
233
- rev: v0.2.0
235
+ rev: v0.2.1
234
236
  hooks:
235
237
  # Formatter: colorizes modified Markdown, LaTeX, and Jupyter files in-place
236
238
  - id: color-math
@@ -175,7 +175,7 @@ Integrate `color-math` into your git workflow using [pre-commit](https://pre-com
175
175
  ```yaml
176
176
  repos:
177
177
  - repo: https://github.com/36ty-blip/color-math
178
- rev: v0.2.0
178
+ rev: v0.2.1
179
179
  hooks:
180
180
  # Formatter: colorizes modified Markdown, LaTeX, and Jupyter files in-place
181
181
  - id: color-math
@@ -5,7 +5,7 @@ from .config import ColorMathOptions
5
5
  from .converters.block import convert_text
6
6
  from .undo import uncolor_text
7
7
 
8
- __version__ = "0.2.0"
8
+ __version__ = "0.2.1"
9
9
 
10
10
  __all__ = [
11
11
  "__version__",
@@ -272,7 +272,7 @@ FONT_STYLE_MACROS = frozenset({
272
272
  })
273
273
 
274
274
 
275
- BARE_FUNCTIONS = frozenset({
275
+ STANDARD_BARE_FUNCTIONS = frozenset({
276
276
  "sin",
277
277
  "cos",
278
278
  "tan",
@@ -313,6 +313,9 @@ BARE_FUNCTIONS = frozenset({
313
313
  "span",
314
314
  "diag",
315
315
  "sgn",
316
+ })
317
+
318
+ EXTENDED_BARE_FUNCTIONS = frozenset({
316
319
  "adj",
317
320
  "col",
318
321
  "row",
@@ -349,15 +352,40 @@ BARE_FUNCTIONS = frozenset({
349
352
  "lg",
350
353
  "lb",
351
354
  "aut",
352
- "end",
353
355
  "gal",
354
356
  "ann",
355
357
  "tor",
356
358
  "ext",
357
359
  "pic",
358
360
  "cl",
361
+ "jac",
362
+ "hes",
363
+ "wr",
364
+ "vol",
365
+ "rms",
366
+ "fft",
367
+ "dft",
368
+ "ord",
369
+ "val",
370
+ "num",
371
+ "den",
372
+ "sn",
373
+ "cn",
374
+ "dn",
375
+ "avg",
376
+ "len",
359
377
  })
360
378
 
379
+ FULL_BARE_FUNCTIONS = STANDARD_BARE_FUNCTIONS | EXTENDED_BARE_FUNCTIONS
380
+ ALL_BARE_FUNCTIONS = FULL_BARE_FUNCTIONS
381
+ BARE_FUNCTIONS = FULL_BARE_FUNCTIONS
382
+
383
+
384
+ def get_bare_functions(options: ColorMathOptions | None = None) -> frozenset[str]:
385
+ if options is not None and not options.extended_functions:
386
+ return STANDARD_BARE_FUNCTIONS
387
+ return FULL_BARE_FUNCTIONS
388
+
361
389
 
362
390
  MATH_ACCENTS = {
363
391
  r"\dot",
@@ -497,6 +525,22 @@ MATH_FUNCTIONS = {
497
525
  r"\ext",
498
526
  r"\pic",
499
527
  r"\cl",
528
+ r"\jac",
529
+ r"\hes",
530
+ r"\wr",
531
+ r"\vol",
532
+ r"\rms",
533
+ r"\fft",
534
+ r"\dft",
535
+ r"\ord",
536
+ r"\val",
537
+ r"\num",
538
+ r"\den",
539
+ r"\sn",
540
+ r"\cn",
541
+ r"\dn",
542
+ r"\avg",
543
+ r"\len",
500
544
  }
501
545
 
502
546
 
@@ -540,8 +584,10 @@ class ColorMathOptions:
540
584
  color_differentials: bool = False
541
585
  color_braket: bool = False
542
586
  color_dimensionless: bool = False
543
- color_alignment: bool = True
544
- color_single_constants: bool = True
587
+ color_alignment: bool = False
588
+ color_single_constants: bool = False
589
+ normalize_braces: bool = False
590
+ extended_functions: bool = True
545
591
 
546
592
  @classmethod
547
593
  def extended(cls) -> ColorMathOptions:
@@ -556,6 +602,8 @@ class ColorMathOptions:
556
602
  color_dimensionless=True,
557
603
  color_alignment=True,
558
604
  color_single_constants=True,
605
+ normalize_braces=False,
606
+ extended_functions=True,
559
607
  )
560
608
 
561
609
  @classmethod
@@ -569,6 +617,10 @@ class ColorMathOptions:
569
617
  color_differentials=True,
570
618
  color_braket=True,
571
619
  color_dimensionless=True,
620
+ color_alignment=True,
621
+ color_single_constants=True,
622
+ normalize_braces=False,
623
+ extended_functions=True,
572
624
  )
573
625
 
574
626
 
@@ -130,18 +130,28 @@ def convert_text(
130
130
 
131
131
  Handles:
132
132
  - single-line and multiline $$ ... $$ math blocks
133
+ - inline $ ... $ math expressions
133
134
  - normal text passthrough
134
135
  """
135
-
136
- math_blocks = scan_markdown(text).math_blocks
137
- if not math_blocks:
136
+ scan = scan_markdown(text)
137
+ all_spans = sorted(
138
+ [*scan.math_blocks, *scan.math_inlines],
139
+ key=lambda s: s.start,
140
+ )
141
+ if not all_spans:
138
142
  return text
139
143
 
140
144
  converted: list[str] = []
141
145
  index = 0
142
- for span in math_blocks:
146
+ for span in all_spans:
143
147
  converted.append(text[index:span.start])
144
- converted.append(convert_math_block(text[span.start:span.end], palette, options))
148
+ if span.kind == "math_inline":
149
+ raw = text[span.content_start:span.content_end]
150
+ converted.append(f"${color_latex_body(raw, palette, options)}$")
151
+ else:
152
+ converted.append(
153
+ convert_math_block(text[span.start:span.end], palette, options)
154
+ )
145
155
  index = span.end
146
156
  converted.append(text[index:])
147
157
  return "".join(converted)
@@ -15,6 +15,7 @@ from ..parsers.latex_spans import (
15
15
  from ..parsers.scanner import collect_operator_spans
16
16
  from ..utils.latex_helpers import contains_color_wrapper
17
17
  from ..utils.spans import ColorSpan, apply_color_spans
18
+ from ..undo import uncolor_text
18
19
  from .semantic import first_equality, parse_math_block, relation_spans, trim_range
19
20
 
20
21
 
@@ -221,7 +222,8 @@ def _rhs_spans(body: str, start: int, end: int | None = None) -> list[ColorSpan]
221
222
 
222
223
  def convert_derivative_line(source: str) -> str | None:
223
224
  """Color a derivative block by inserting wrappers into its exact source."""
224
- block = parse_math_block(source)
225
+ clean_source = uncolor_text(source) if contains_color_wrapper(source) else source
226
+ block = parse_math_block(clean_source)
225
227
  if block is None:
226
228
  return None
227
229
 
@@ -229,8 +231,6 @@ def convert_derivative_line(source: str) -> str | None:
229
231
  prefix = read_operand(block.body, body_start)
230
232
  if prefix is None or not _is_derivative_prefix(prefix.text(block.body)):
231
233
  return None
232
- if contains_color_wrapper(block.body):
233
- return source
234
234
 
235
235
  equality = first_equality(block.body)
236
236
  if equality is None or equality[0] <= prefix.end:
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
 
7
- from ..config import COLORS, ColorMathOptions
7
+ from ..config import COLORS, ColorMathOptions, get_bare_functions, BARE_FUNCTIONS
8
8
  from ..parsers.constants import collect_single_constant_spans
9
9
  from ..parsers.math_parser import find_semantic_spans
10
10
  from ..parsers.scanner import collect_scanner_spans
@@ -17,6 +17,7 @@ from ..parsers.taxonomy import collect_taxonomy_spans
17
17
  from ..parsers.variable_hash import collect_variable_spans
18
18
  from ..utils.latex_helpers import contains_color_wrapper, normalize_latex_braces
19
19
  from ..utils.spans import ColorSpan, apply_color_spans
20
+ from ..undo import uncolor_fragment
20
21
 
21
22
 
22
23
  MATH_LINE_RE = re.compile(
@@ -25,10 +26,14 @@ MATH_LINE_RE = re.compile(
25
26
  FUNCTION_COLOR_NAMES = ("main", "derivative", "chain")
26
27
 
27
28
 
28
- def collect_function_spans(body: str, palette: dict[str, str] | None = None) -> list[ColorSpan]:
29
+ def collect_function_spans(
30
+ body: str,
31
+ palette: dict[str, str] | None = None,
32
+ bare_functions: frozenset[str] | None = None,
33
+ ) -> list[ColorSpan]:
29
34
  """Color nested call names and recognize literal constants."""
30
35
  pal = palette or COLORS
31
- semantic, _ = find_semantic_spans(body)
36
+ semantic, _ = find_semantic_spans(body, bare_functions or BARE_FUNCTIONS)
32
37
  spans: list[ColorSpan] = []
33
38
  for item in semantic:
34
39
  if item.kind == "function":
@@ -49,21 +54,38 @@ def color_latex_body(
49
54
  options: ColorMathOptions | None = None,
50
55
  ) -> str:
51
56
  """Insert scoped colors while preserving every original source character."""
52
- if contains_color_wrapper(body):
53
- return body
57
+ clean_body = uncolor_fragment(body) if contains_color_wrapper(body) else body
54
58
 
55
59
  pal = palette or COLORS
56
60
  opts = options or ColorMathOptions()
57
61
 
58
- normalized = normalize_latex_braces(body)
62
+ normalized = normalize_latex_braces(clean_body) if opts.normalize_braces else clean_body
63
+
64
+ need_units = opts.color_units or opts.enable_taxonomy or opts.variable_data_flow
65
+ need_diffs = opts.color_differentials or opts.enable_taxonomy or opts.variable_data_flow
66
+ need_dims = opts.color_dimensionless or opts.enable_taxonomy or opts.variable_data_flow
67
+
68
+ unit_spans = find_unit_spans(normalized) if need_units else []
69
+ diff_spans = find_differential_spans(normalized) if need_diffs else []
70
+ dim_spans = find_dimensionless_spans(normalized) if need_dims else []
71
+
72
+ bare_functions = (
73
+ get_bare_functions(opts)
74
+ if (opts.enable_taxonomy or opts.variable_data_flow)
75
+ else frozenset()
76
+ )
59
77
 
60
- unit_spans = find_unit_spans(normalized)
61
- diff_spans = find_differential_spans(normalized)
62
- dim_spans = find_dimensionless_spans(normalized)
78
+ scanner_spans = collect_scanner_spans(normalized)
79
+ protected = [*unit_spans, *diff_spans, *dim_spans]
80
+ if protected:
81
+ scanner_spans = [
82
+ s for s in scanner_spans
83
+ if not any(p.start <= s.start and s.end <= p.end for p in protected)
84
+ ]
63
85
 
64
86
  spans: list[ColorSpan] = [
65
- *collect_function_spans(normalized, pal),
66
- *collect_scanner_spans(normalized),
87
+ *collect_function_spans(normalized, pal, bare_functions),
88
+ *scanner_spans,
67
89
  ]
68
90
 
69
91
  if opts.color_units:
@@ -88,7 +110,7 @@ def color_latex_body(
88
110
  spans.extend(collect_taxonomy_spans(normalized, pal, unit_spans, diff_spans, dim_spans))
89
111
 
90
112
  if opts.variable_data_flow:
91
- spans.extend(collect_variable_spans(normalized, None, unit_spans, diff_spans, dim_spans))
113
+ spans.extend(collect_variable_spans(normalized, None, unit_spans, diff_spans, dim_spans, bare_functions))
92
114
 
93
115
  return apply_color_spans(normalized, spans)
94
116
 
@@ -9,6 +9,7 @@ from ..parsers.latex_spans import OperandSpan, find_operand_spans, read_operand
9
9
  from ..parsers.scanner import collect_operator_spans, collect_structured_spans
10
10
  from ..utils.latex_helpers import contains_color_wrapper
11
11
  from ..utils.spans import ColorSpan, apply_color_spans
12
+ from ..undo import uncolor_text
12
13
  from .semantic import first_equality, parse_math_block, relation_spans
13
14
 
14
15
 
@@ -79,6 +80,8 @@ def _operand_color_spans(
79
80
  spans: list[ColorSpan] = []
80
81
  semantic_index = 0
81
82
  for operand in operands:
83
+ if operand.kind in ("matrix", "environment"):
84
+ continue
82
85
  value = re.sub(r"\s+", "", operand.text(body))
83
86
  if NUMBER_RE.fullmatch(value):
84
87
  name = "orange"
@@ -102,11 +105,10 @@ def _operator_spans(body: str) -> list[ColorSpan]:
102
105
 
103
106
  def convert_matrix_block(source: str) -> str | None:
104
107
  """Color complete matrix/tensor operands without reconstructing LaTeX."""
105
- block = parse_math_block(source)
108
+ clean_source = uncolor_text(source) if contains_color_wrapper(source) else source
109
+ block = parse_math_block(clean_source)
106
110
  if block is None or not _is_matrix_expression(block.body):
107
111
  return None
108
- if contains_color_wrapper(block.body):
109
- return source
110
112
 
111
113
  equality = first_equality(block.body)
112
114
  if equality is None:
@@ -32,7 +32,7 @@ from .parsers.math_parser import describe_math_blocks
32
32
  from .self_test import run_self_test
33
33
  from .tutorial import run_tutorial
34
34
 
35
- VERSION = "0.2.0"
35
+ VERSION = "0.2.1"
36
36
  SUPPORTED_EXTENSIONS = {".md", ".markdown", ".qmd", ".ipynb", ".tex", ".latex"}
37
37
  DEFAULT_EXCLUDES = {
38
38
  ".git",
@@ -297,6 +297,11 @@ def build_parser() -> argparse.ArgumentParser:
297
297
  action="store_true",
298
298
  help="Open the interactive graphical window with preview, colors, and settings.",
299
299
  )
300
+ interactive_group.add_argument(
301
+ "--mcp",
302
+ action="store_true",
303
+ help="Launch the Model Context Protocol (MCP) server over stdio for AI assistants.",
304
+ )
300
305
  interactive_group.add_argument(
301
306
  "--tutorial",
302
307
  action="store_true",
@@ -584,6 +589,16 @@ def _main_impl(argv: list[str] | None = None) -> int:
584
589
  options=options,
585
590
  )
586
591
 
592
+ # Launch Model Context Protocol (MCP) Server
593
+ if args.mcp:
594
+ try:
595
+ from .mcp_server import run_mcp_server
596
+ except ImportError as exc:
597
+ sys.stderr.write(f"Error starting Color Math MCP server:\n{exc}\n")
598
+ return 1
599
+ run_mcp_server(transport="stdio")
600
+ return 0
601
+
587
602
  # Generate shell autocompletion script
588
603
  if args.generate_completion:
589
604
  script = get_completion_script(args.generate_completion)
@@ -0,0 +1,42 @@
1
+ """Direct module entrypoint for running the Color Math MCP server: python -m color_math.mcp"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+
9
+ def main() -> None:
10
+ parser = argparse.ArgumentParser(
11
+ prog="color-math-mcp",
12
+ description="Run the Python Color Math Model Context Protocol (MCP) server.",
13
+ )
14
+ parser.add_argument(
15
+ "--transport",
16
+ choices=["stdio", "sse", "streamable-http"],
17
+ default="stdio",
18
+ help="Transport protocol (default: stdio)",
19
+ )
20
+ parser.add_argument(
21
+ "--port",
22
+ type=int,
23
+ default=8000,
24
+ help="Port for SSE or HTTP transport (default: 8000)",
25
+ )
26
+ args = parser.parse_args()
27
+
28
+ try:
29
+ from .mcp_server import run_mcp_server
30
+ except ImportError as e:
31
+ print(f"Error starting Color Math MCP server:\n{e}", file=sys.stderr)
32
+ sys.exit(1)
33
+
34
+ extra_kwargs = {}
35
+ if args.transport in {"sse", "streamable-http"}:
36
+ extra_kwargs["port"] = args.port
37
+
38
+ run_mcp_server(transport=args.transport, **extra_kwargs)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()