modelable 1.0.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.

Potentially problematic release.


This version of modelable might be problematic. Click here for more details.

Files changed (122) hide show
  1. modelable/__init__.py +1 -0
  2. modelable/__main__.py +3 -0
  3. modelable/_pydantic_py314_compat.py +31 -0
  4. modelable/cli.py +41 -0
  5. modelable/commands/__init__.py +1 -0
  6. modelable/commands/apicurio.py +84 -0
  7. modelable/commands/codegen.py +241 -0
  8. modelable/commands/common.py +43 -0
  9. modelable/commands/compile.py +237 -0
  10. modelable/commands/create.py +164 -0
  11. modelable/commands/diff.py +82 -0
  12. modelable/commands/graph.py +53 -0
  13. modelable/commands/llm.py +564 -0
  14. modelable/commands/lsp.py +15 -0
  15. modelable/commands/runtime.py +37 -0
  16. modelable/commands/scenario.py +104 -0
  17. modelable/commands/spec.py +197 -0
  18. modelable/commands/workspace.py +240 -0
  19. modelable/compat/__init__.py +11 -0
  20. modelable/compat/checker.py +179 -0
  21. modelable/compat/diff.py +169 -0
  22. modelable/compiler/__init__.py +3 -0
  23. modelable/compiler/compiler.py +19 -0
  24. modelable/compiler/workspace.py +346 -0
  25. modelable/diagnostics/__init__.py +3 -0
  26. modelable/diagnostics/model.py +27 -0
  27. modelable/emitters/__init__.py +0 -0
  28. modelable/emitters/base.py +22 -0
  29. modelable/emitters/csharp.py +245 -0
  30. modelable/emitters/dbt_yaml.py +290 -0
  31. modelable/emitters/diagnostics.py +25 -0
  32. modelable/emitters/fhir.py +694 -0
  33. modelable/emitters/fhir_validator.py +36 -0
  34. modelable/emitters/go.py +334 -0
  35. modelable/emitters/java.py +264 -0
  36. modelable/emitters/json_schema.py +458 -0
  37. modelable/emitters/markdown.py +252 -0
  38. modelable/emitters/odcs.py +355 -0
  39. modelable/emitters/openlineage.py +315 -0
  40. modelable/emitters/openmetadata.py +258 -0
  41. modelable/emitters/python.py +282 -0
  42. modelable/emitters/rust.py +643 -0
  43. modelable/emitters/shapes.py +261 -0
  44. modelable/emitters/sql.py +266 -0
  45. modelable/emitters/targets.py +141 -0
  46. modelable/emitters/typescript.py +352 -0
  47. modelable/expressions/__init__.py +0 -0
  48. modelable/expressions/cel.py +547 -0
  49. modelable/governance/__init__.py +3 -0
  50. modelable/governance/checker.py +271 -0
  51. modelable/governance/por.py +46 -0
  52. modelable/grammar/__init__.py +1 -0
  53. modelable/grammar/modelable.lark +257 -0
  54. modelable/graph/__init__.py +5 -0
  55. modelable/graph/export.py +442 -0
  56. modelable/llm/__init__.py +43 -0
  57. modelable/llm/chat.py +255 -0
  58. modelable/llm/config.py +87 -0
  59. modelable/llm/context.py +194 -0
  60. modelable/llm/engine.py +976 -0
  61. modelable/llm/importers.py +1077 -0
  62. modelable/llm/provenance.py +84 -0
  63. modelable/llm/providers.py +182 -0
  64. modelable/llm/qa.py +126 -0
  65. modelable/llm/recommendations.py +33 -0
  66. modelable/llm/redaction.py +19 -0
  67. modelable/llm/render.py +279 -0
  68. modelable/llm/update_plan.py +101 -0
  69. modelable/llm/validation_help.py +10 -0
  70. modelable/lsp/__init__.py +3 -0
  71. modelable/lsp/__main__.py +4 -0
  72. modelable/lsp/code_actions.py +210 -0
  73. modelable/lsp/completion.py +480 -0
  74. modelable/lsp/definition.py +343 -0
  75. modelable/lsp/diagnostics.py +31 -0
  76. modelable/lsp/document_symbols.py +197 -0
  77. modelable/lsp/federation.py +261 -0
  78. modelable/lsp/folding.py +33 -0
  79. modelable/lsp/formatting.py +64 -0
  80. modelable/lsp/highlight.py +30 -0
  81. modelable/lsp/hover.py +370 -0
  82. modelable/lsp/inlay_hints.py +158 -0
  83. modelable/lsp/references.py +511 -0
  84. modelable/lsp/rename.py +564 -0
  85. modelable/lsp/semantic_tokens.py +412 -0
  86. modelable/lsp/server.py +370 -0
  87. modelable/lsp/workspace.py +83 -0
  88. modelable/lsp/workspace_symbols.py +104 -0
  89. modelable/parser/__init__.py +94 -0
  90. modelable/parser/ir.py +451 -0
  91. modelable/parser/parse.py +47 -0
  92. modelable/parser/transformer.py +798 -0
  93. modelable/parser/wire.py +68 -0
  94. modelable/planner/__init__.py +0 -0
  95. modelable/planner/lineage.py +91 -0
  96. modelable/planner/planner.py +134 -0
  97. modelable/planner/plans.py +122 -0
  98. modelable/py.typed +0 -0
  99. modelable/registry/__init__.py +9 -0
  100. modelable/registry/apicurio.py +166 -0
  101. modelable/registry/base.py +18 -0
  102. modelable/registry/factory.py +18 -0
  103. modelable/registry/index.py +419 -0
  104. modelable/registry/local.py +26 -0
  105. modelable/registry/oci.py +22 -0
  106. modelable/registry/resolver.py +213 -0
  107. modelable/registry/schema.sql +119 -0
  108. modelable/registry/signature.py +26 -0
  109. modelable/release.py +125 -0
  110. modelable/runtime/__init__.py +5 -0
  111. modelable/runtime/adapter/__init__.py +17 -0
  112. modelable/runtime/adapter/base.py +18 -0
  113. modelable/runtime/adapter/postgres.py +82 -0
  114. modelable/specs/__init__.py +23 -0
  115. modelable/specs/tracking.py +220 -0
  116. modelable/validation/__init__.py +3 -0
  117. modelable/validation/semantic.py +659 -0
  118. modelable-1.0.0.dist-info/METADATA +61 -0
  119. modelable-1.0.0.dist-info/RECORD +122 -0
  120. modelable-1.0.0.dist-info/WHEEL +4 -0
  121. modelable-1.0.0.dist-info/entry_points.txt +2 -0
  122. modelable-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,511 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from lsprotocol import types
6
+
7
+ from modelable.llm.context import parse_model_ref
8
+ from modelable.lsp.workspace import LspWorkspaceIndex
9
+ from modelable.registry.resolver import resolve_model_ref
10
+
11
+ _QUALIFIED_REF_PATTERN = re.compile(
12
+ r"(?P<domain>[A-Za-z_][A-Za-z0-9_]*)\.(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
13
+ )
14
+ _FIELD_REF_PATTERN = re.compile(r"(?P<alias>[A-Za-z_][A-Za-z0-9_]*)\.(?P<field>[A-Za-z_][A-Za-z0-9_]*)")
15
+ _DECL_PATTERN = re.compile(
16
+ r"^\s*(?P<kind>entity|aggregate|event|value|projection)\s+"
17
+ r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
18
+ )
19
+ _DOMAIN_PATTERN = re.compile(r'^\s*domain\s+(?:"(?P<quoted>[^"]+)"|(?P<name>[A-Za-z_][A-Za-z0-9_]*))')
20
+ _WORD_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
21
+ _MODEL_FIELD_PATTERN = re.compile(
22
+ r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\??\s*:"
23
+ )
24
+ _PROJECTION_FIELD_PATTERN = re.compile(
25
+ r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)"
26
+ )
27
+ _MODEL_DECL_KINDS = {"entity", "aggregate", "event", "value"}
28
+
29
+
30
+ def build_references(
31
+ index: LspWorkspaceIndex,
32
+ uri: str,
33
+ line: int,
34
+ character: int,
35
+ include_declaration: bool,
36
+ ) -> list[types.Location] | None:
37
+ source = index.documents.get(uri)
38
+ workspace = index.workspace
39
+ if source is None or workspace is None:
40
+ return None
41
+
42
+ lines = source.text.splitlines()
43
+ if line < 0 or line >= len(lines):
44
+ return None
45
+ text_line = lines[line]
46
+
47
+ for match in _QUALIFIED_REF_PATTERN.finditer(text_line):
48
+ if _contains(match.start(), match.end(), character):
49
+ ref = f"{match.group('domain')}.{match.group('name')}@{match.group('version')}"
50
+ return _references_for_qualified_ref(workspace, ref, include_declaration)
51
+
52
+ for match in _FIELD_REF_PATTERN.finditer(text_line):
53
+ if _contains(match.start(), match.end(), character):
54
+ return _references_for_field_reference(
55
+ workspace,
56
+ source.text,
57
+ line,
58
+ match.group("alias"),
59
+ match.group("field"),
60
+ include_declaration,
61
+ )
62
+
63
+ word = _word_at(text_line, character)
64
+ if word is None:
65
+ return None
66
+
67
+ scope = _current_scope(source.text, line)
68
+ if scope is None:
69
+ return None
70
+ domain_name, kind, name, version = scope
71
+
72
+ if word == name:
73
+ return _references_for_decl(workspace, domain_name, kind, name, version, include_declaration)
74
+
75
+ if kind == "model":
76
+ return _references_for_source_field(
77
+ workspace,
78
+ domain_name,
79
+ name,
80
+ version,
81
+ word,
82
+ include_declaration,
83
+ )
84
+
85
+ return _references_for_projection_field(
86
+ workspace,
87
+ domain_name,
88
+ name,
89
+ version,
90
+ word,
91
+ include_declaration,
92
+ )
93
+
94
+
95
+ def _references_for_qualified_ref(
96
+ workspace,
97
+ ref: str,
98
+ include_declaration: bool,
99
+ ) -> list[types.Location]:
100
+ model_ref = parse_model_ref(ref)
101
+ domain = next((d for d in workspace.mdl.domains if d.name == model_ref.domain), None)
102
+ if domain is None:
103
+ return []
104
+
105
+ if model_ref.name in domain.models:
106
+ kind = "model"
107
+ elif model_ref.name in domain.projections:
108
+ kind = "projection"
109
+ else:
110
+ return []
111
+
112
+ locations = _reference_locations_for_decl(workspace, model_ref.domain, kind, model_ref.name, model_ref.version)
113
+ if include_declaration:
114
+ decl = _find_decl_location(workspace, model_ref.domain, kind, model_ref.name, model_ref.version)
115
+ if decl is not None:
116
+ locations = [decl, *locations]
117
+ return _dedupe_locations(locations)
118
+
119
+
120
+ def _references_for_field_reference(
121
+ workspace,
122
+ text: str,
123
+ line: int,
124
+ alias: str,
125
+ field_name: str,
126
+ include_declaration: bool,
127
+ ) -> list[types.Location]:
128
+ scope = _current_scope(text, line)
129
+ if scope is None:
130
+ return []
131
+
132
+ domain_name, kind, name, version = scope
133
+ if kind == "model":
134
+ return _references_for_source_field(
135
+ workspace,
136
+ domain_name,
137
+ name,
138
+ version,
139
+ field_name,
140
+ include_declaration,
141
+ )
142
+
143
+ domain = next((d for d in workspace.mdl.domains if d.name == domain_name), None)
144
+ if domain is None:
145
+ return []
146
+ versions = domain.projections.get(name, [])
147
+ projection_version = next((item for item in versions if item.version == version), None)
148
+ if projection_version is None:
149
+ return []
150
+
151
+ for source_ref in [projection_version.source, *projection_version.joins]:
152
+ if source_ref.alias != alias:
153
+ continue
154
+ try:
155
+ resolved = resolve_model_ref(
156
+ workspace.mdl,
157
+ source_ref.model,
158
+ source_ref.version,
159
+ )
160
+ except LookupError:
161
+ continue
162
+ return _references_for_source_field(
163
+ workspace,
164
+ resolved.domain_name,
165
+ resolved.model_name,
166
+ resolved.version.version,
167
+ field_name,
168
+ include_declaration,
169
+ )
170
+
171
+ if include_declaration:
172
+ location = _find_field_location(
173
+ workspace,
174
+ domain_name,
175
+ "projection",
176
+ name,
177
+ version,
178
+ field_name,
179
+ )
180
+ return [location] if location is not None else []
181
+ return []
182
+
183
+
184
+ def _references_for_decl(
185
+ workspace,
186
+ domain_name: str,
187
+ kind: str,
188
+ name: str,
189
+ version: int,
190
+ include_declaration: bool,
191
+ ) -> list[types.Location]:
192
+ locations = _reference_locations_for_decl(workspace, domain_name, kind, name, version)
193
+ if include_declaration:
194
+ decl = _find_decl_location(workspace, domain_name, kind, name, version)
195
+ if decl is not None:
196
+ locations = [decl, *locations]
197
+ return _dedupe_locations(locations)
198
+
199
+
200
+ def _references_for_source_field(
201
+ workspace,
202
+ domain_name: str,
203
+ model_name: str,
204
+ version: int,
205
+ field_name: str,
206
+ include_declaration: bool,
207
+ ) -> list[types.Location]:
208
+ locations: list[types.Location] = []
209
+ if include_declaration:
210
+ decl = _find_source_field_location(workspace, domain_name, model_name, version, field_name)
211
+ if decl is not None:
212
+ locations.append(decl)
213
+
214
+ for source in workspace.sources:
215
+ current_domain: str | None = None
216
+ current_projection: tuple[str, int] | None = None
217
+ alias_map: dict[str, tuple[str, str, int]] = {}
218
+ lines = source.text.splitlines()
219
+
220
+ for line_no, line_text in enumerate(lines):
221
+ domain_match = _DOMAIN_PATTERN.match(line_text)
222
+ if domain_match:
223
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
224
+ current_projection = None
225
+ alias_map = {}
226
+ continue
227
+
228
+ decl_match = _DECL_PATTERN.match(line_text)
229
+ if decl_match and current_domain is not None:
230
+ if decl_match.group("kind") == "projection":
231
+ current_projection = (decl_match.group("name"), int(decl_match.group("version")))
232
+ alias_map = _projection_aliases(workspace, current_domain, *current_projection)
233
+ else:
234
+ current_projection = None
235
+ alias_map = {}
236
+ continue
237
+
238
+ if current_projection is None:
239
+ continue
240
+
241
+ for match in _FIELD_REF_PATTERN.finditer(line_text):
242
+ target = alias_map.get(match.group("alias"))
243
+ if target is None:
244
+ continue
245
+ if target != (domain_name, model_name, version):
246
+ continue
247
+ if match.group("field") != field_name:
248
+ continue
249
+ if not _field_exists(workspace, *target, field_name):
250
+ continue
251
+ locations.append(
252
+ types.Location(
253
+ uri=source.uri,
254
+ range=types.Range(
255
+ start=types.Position(line=line_no, character=match.start()),
256
+ end=types.Position(line=line_no, character=match.end()),
257
+ ),
258
+ )
259
+ )
260
+
261
+ return _dedupe_locations(locations)
262
+
263
+
264
+ def _references_for_projection_field(
265
+ workspace,
266
+ domain_name: str,
267
+ projection_name: str,
268
+ version: int,
269
+ field_name: str,
270
+ include_declaration: bool,
271
+ ) -> list[types.Location]:
272
+ return _references_for_source_field(
273
+ workspace,
274
+ domain_name,
275
+ projection_name,
276
+ version,
277
+ field_name,
278
+ include_declaration,
279
+ )
280
+
281
+
282
+ def _reference_locations_for_decl(
283
+ workspace,
284
+ domain_name: str,
285
+ kind: str,
286
+ name: str,
287
+ version: int,
288
+ ) -> list[types.Location]:
289
+ ref = f"{domain_name}.{name}@{version}"
290
+ locations: list[types.Location] = []
291
+ for source in workspace.sources:
292
+ lines = source.text.splitlines()
293
+ for line_no, line_text in enumerate(lines):
294
+ for match in _QUALIFIED_REF_PATTERN.finditer(line_text):
295
+ candidate = f"{match.group('domain')}.{match.group('name')}@{match.group('version')}"
296
+ if candidate != ref:
297
+ continue
298
+ locations.append(
299
+ types.Location(
300
+ uri=source.uri,
301
+ range=types.Range(
302
+ start=types.Position(line=line_no, character=match.start()),
303
+ end=types.Position(line=line_no, character=match.end()),
304
+ ),
305
+ )
306
+ )
307
+ return locations
308
+
309
+
310
+ def _projection_aliases(
311
+ workspace,
312
+ domain_name: str,
313
+ projection_name: str,
314
+ version: int,
315
+ ) -> dict[str, tuple[str, str, int]]:
316
+ domain = next((item for item in workspace.mdl.domains if item.name == domain_name), None)
317
+ if domain is None:
318
+ return {}
319
+ versions = domain.projections.get(projection_name, [])
320
+ projection_version = next((item for item in versions if item.version == version), None)
321
+ if projection_version is None:
322
+ return {}
323
+
324
+ aliases: dict[str, tuple[str, str, int]] = {}
325
+ for source_ref in [projection_version.source, *projection_version.joins]:
326
+ try:
327
+ resolved = resolve_model_ref(workspace.mdl, source_ref.model, source_ref.version)
328
+ except LookupError:
329
+ continue
330
+ aliases[source_ref.alias] = (
331
+ resolved.domain_name,
332
+ resolved.model_name,
333
+ resolved.version.version,
334
+ )
335
+ return aliases
336
+
337
+
338
+ def _field_exists(workspace, domain_name: str, model_name: str, version: int, field_name: str) -> bool:
339
+ source_version = _source_version(workspace, domain_name, model_name, version)
340
+ if source_version is None:
341
+ return False
342
+ return any(field.name == field_name for field in getattr(source_version, "fields", []))
343
+
344
+
345
+ def _source_version(workspace, domain_name: str, model_name: str, version: int):
346
+ domain = next((item for item in workspace.mdl.domains if item.name == domain_name), None)
347
+ if domain is None:
348
+ return None
349
+ versions = domain.models.get(model_name, [])
350
+ source_version = next((item for item in versions if item.version == version), None)
351
+ if source_version is not None:
352
+ return source_version
353
+ versions = domain.projections.get(model_name, [])
354
+ return next((item for item in versions if item.version == version), None)
355
+
356
+
357
+ def _find_source_field_location(
358
+ workspace,
359
+ domain_name: str,
360
+ model_name: str,
361
+ version: int,
362
+ field_name: str,
363
+ ) -> types.Location | None:
364
+ for kind in ("model", "projection"):
365
+ location = _find_field_location(
366
+ workspace,
367
+ domain_name,
368
+ kind,
369
+ model_name,
370
+ version,
371
+ field_name,
372
+ )
373
+ if location is not None:
374
+ return location
375
+ return None
376
+
377
+
378
+ def _find_decl_location(
379
+ workspace,
380
+ domain_name: str,
381
+ kind: str,
382
+ name: str,
383
+ version: int,
384
+ ) -> types.Location | None:
385
+ for source in workspace.sources:
386
+ current_domain: str | None = None
387
+ lines = source.text.splitlines()
388
+ for line_no, line_text in enumerate(lines):
389
+ domain_match = _DOMAIN_PATTERN.match(line_text)
390
+ if domain_match:
391
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
392
+ continue
393
+ decl_match = _DECL_PATTERN.match(line_text)
394
+ if not decl_match or current_domain != domain_name:
395
+ continue
396
+ decl_kind = decl_match.group("kind")
397
+ if kind == "model":
398
+ if decl_kind not in _MODEL_DECL_KINDS:
399
+ continue
400
+ elif decl_kind != kind:
401
+ continue
402
+ if decl_match.group("name") != name:
403
+ continue
404
+ if int(decl_match.group("version")) != version:
405
+ continue
406
+ return types.Location(
407
+ uri=source.uri,
408
+ range=types.Range(
409
+ start=types.Position(line=line_no, character=decl_match.start("name")),
410
+ end=types.Position(line=line_no, character=decl_match.end("name")),
411
+ ),
412
+ )
413
+ return None
414
+
415
+
416
+ def _find_field_location(
417
+ workspace,
418
+ domain_name: str,
419
+ kind: str,
420
+ name: str,
421
+ version: int,
422
+ field_name: str,
423
+ ) -> types.Location | None:
424
+ for source in workspace.sources:
425
+ current_domain: str | None = None
426
+ active = False
427
+ lines = source.text.splitlines()
428
+ pattern = _MODEL_FIELD_PATTERN if kind == "model" else _PROJECTION_FIELD_PATTERN
429
+
430
+ for line_no, line_text in enumerate(lines):
431
+ domain_match = _DOMAIN_PATTERN.match(line_text)
432
+ if domain_match:
433
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
434
+ active = False
435
+ continue
436
+ decl_match = _DECL_PATTERN.match(line_text)
437
+ if decl_match and current_domain == domain_name:
438
+ decl_kind = decl_match.group("kind")
439
+ active = (
440
+ (decl_kind in _MODEL_DECL_KINDS if kind == "model" else decl_kind == kind)
441
+ and decl_match.group("name") == name
442
+ and int(decl_match.group("version")) == version
443
+ )
444
+ continue
445
+ if not active:
446
+ continue
447
+ field_match = pattern.match(line_text)
448
+ if field_match and field_match.group("name") == field_name:
449
+ return types.Location(
450
+ uri=source.uri,
451
+ range=types.Range(
452
+ start=types.Position(line=line_no, character=field_match.start("name")),
453
+ end=types.Position(line=line_no, character=field_match.end("name")),
454
+ ),
455
+ )
456
+ if line_text.strip() == "}":
457
+ active = False
458
+ return None
459
+
460
+
461
+ def _dedupe_locations(locations: list[types.Location]) -> list[types.Location]:
462
+ seen: set[tuple[str, int, int, int, int]] = set()
463
+ deduped: list[types.Location] = []
464
+ for location in locations:
465
+ key = (
466
+ location.uri,
467
+ location.range.start.line,
468
+ location.range.start.character,
469
+ location.range.end.line,
470
+ location.range.end.character,
471
+ )
472
+ if key in seen:
473
+ continue
474
+ seen.add(key)
475
+ deduped.append(location)
476
+ return deduped
477
+
478
+
479
+ def _current_scope(text: str, line: int) -> tuple[str, str, str, int] | None:
480
+ lines = text.splitlines()
481
+ current_domain: str | None = None
482
+ current_kind: str | None = None
483
+ current_name: str | None = None
484
+ current_version: int | None = None
485
+ for item in lines[: line + 1]:
486
+ domain_match = _DOMAIN_PATTERN.match(item)
487
+ if domain_match:
488
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
489
+ current_kind = None
490
+ current_name = None
491
+ current_version = None
492
+ continue
493
+ decl_match = _DECL_PATTERN.match(item)
494
+ if decl_match and current_domain is not None:
495
+ current_kind = "model" if decl_match.group("kind") != "projection" else "projection"
496
+ current_name = decl_match.group("name")
497
+ current_version = int(decl_match.group("version"))
498
+ if current_domain and current_kind and current_name and current_version is not None:
499
+ return current_domain, current_kind, current_name, current_version
500
+ return None
501
+
502
+
503
+ def _word_at(text_line: str, character: int) -> str | None:
504
+ for match in _WORD_PATTERN.finditer(text_line):
505
+ if _contains(match.start(), match.end(), character):
506
+ return match.group(0)
507
+ return None
508
+
509
+
510
+ def _contains(start: int, end: int, character: int) -> bool:
511
+ return start <= character <= max(end - 1, start)