otterapi 0.0.5__py3-none-any.whl → 0.0.6__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.
Files changed (52) hide show
  1. README.md +581 -8
  2. otterapi/__init__.py +73 -0
  3. otterapi/cli.py +327 -29
  4. otterapi/codegen/__init__.py +115 -0
  5. otterapi/codegen/ast_utils.py +134 -5
  6. otterapi/codegen/client.py +1271 -0
  7. otterapi/codegen/codegen.py +1736 -0
  8. otterapi/codegen/dataframes.py +392 -0
  9. otterapi/codegen/emitter.py +473 -0
  10. otterapi/codegen/endpoints.py +2597 -343
  11. otterapi/codegen/pagination.py +1026 -0
  12. otterapi/codegen/schema.py +593 -0
  13. otterapi/codegen/splitting.py +1397 -0
  14. otterapi/codegen/types.py +1345 -0
  15. otterapi/codegen/utils.py +180 -1
  16. otterapi/config.py +1017 -24
  17. otterapi/exceptions.py +231 -0
  18. otterapi/openapi/__init__.py +46 -0
  19. otterapi/openapi/v2/__init__.py +86 -0
  20. otterapi/openapi/v2/spec.json +1607 -0
  21. otterapi/openapi/v2/v2.py +1776 -0
  22. otterapi/openapi/v3/__init__.py +131 -0
  23. otterapi/openapi/v3/spec.json +1651 -0
  24. otterapi/openapi/v3/v3.py +1557 -0
  25. otterapi/openapi/v3_1/__init__.py +133 -0
  26. otterapi/openapi/v3_1/spec.json +1411 -0
  27. otterapi/openapi/v3_1/v3_1.py +798 -0
  28. otterapi/openapi/v3_2/__init__.py +133 -0
  29. otterapi/openapi/v3_2/spec.json +1666 -0
  30. otterapi/openapi/v3_2/v3_2.py +777 -0
  31. otterapi/tests/__init__.py +3 -0
  32. otterapi/tests/fixtures/__init__.py +455 -0
  33. otterapi/tests/test_ast_utils.py +680 -0
  34. otterapi/tests/test_codegen.py +610 -0
  35. otterapi/tests/test_dataframe.py +1038 -0
  36. otterapi/tests/test_exceptions.py +493 -0
  37. otterapi/tests/test_openapi_support.py +616 -0
  38. otterapi/tests/test_openapi_upgrade.py +215 -0
  39. otterapi/tests/test_pagination.py +1101 -0
  40. otterapi/tests/test_splitting_config.py +319 -0
  41. otterapi/tests/test_splitting_integration.py +427 -0
  42. otterapi/tests/test_splitting_resolver.py +512 -0
  43. otterapi/tests/test_splitting_tree.py +525 -0
  44. otterapi-0.0.6.dist-info/METADATA +627 -0
  45. otterapi-0.0.6.dist-info/RECORD +48 -0
  46. {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/WHEEL +1 -1
  47. otterapi/codegen/generator.py +0 -358
  48. otterapi/codegen/openapi_processor.py +0 -27
  49. otterapi/codegen/type_generator.py +0 -559
  50. otterapi-0.0.5.dist-info/METADATA +0 -54
  51. otterapi-0.0.5.dist-info/RECORD +0 -16
  52. {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,593 @@
1
+ """Schema loading and resolution utilities for OpenAPI documents.
2
+
3
+ This module provides utilities for:
4
+ - Loading OpenAPI schemas from various sources (URLs, local files, YAML/JSON)
5
+ - Automatic version detection and upgrading of older OpenAPI/Swagger specifications
6
+ - Resolving $ref references and managing schema lookups in OpenAPI documents
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import Any
13
+ from urllib.parse import urljoin, urlparse
14
+
15
+ import httpx
16
+ import yaml
17
+ from pydantic import TypeAdapter
18
+
19
+ from otterapi.exceptions import (
20
+ SchemaLoadError,
21
+ SchemaReferenceError,
22
+ SchemaValidationError,
23
+ )
24
+ from otterapi.openapi import UniversalOpenAPI
25
+ from otterapi.openapi.v3_2 import Reference, Schema
26
+ from otterapi.openapi.v3_2.v3_2 import OpenAPI as OpenAPIv3
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ __all__ = [
31
+ 'SchemaLoader',
32
+ 'SchemaResolver',
33
+ ]
34
+
35
+
36
+ # =============================================================================
37
+ # Schema Loader
38
+ # =============================================================================
39
+
40
+
41
+ class SchemaLoader:
42
+ """Loads OpenAPI schemas from URLs or file paths.
43
+
44
+ This class provides a unified interface for loading OpenAPI schemas
45
+ from different sources (HTTP URLs or local files), supporting both
46
+ JSON and YAML formats. It also handles automatic version detection
47
+ and upgrading of Swagger 2.0 and older OpenAPI 3.x specifications.
48
+
49
+ Features:
50
+ - Load from URLs (http/https) or local file paths
51
+ - Support for both JSON and YAML formats
52
+ - Automatic version detection (Swagger 2.0, OpenAPI 3.0, 3.1, 3.2)
53
+ - Automatic upgrade to OpenAPI 3.2 for code generation
54
+ - External $ref resolution for URLs and relative files
55
+ - Caching of loaded external schemas
56
+
57
+ Example:
58
+ >>> loader = SchemaLoader()
59
+ >>> schema = loader.load('https://api.example.com/openapi.json')
60
+ >>> # or
61
+ >>> schema = loader.load('/path/to/openapi.yaml')
62
+ >>> # or with external ref resolution
63
+ >>> loader = SchemaLoader(resolve_external_refs=True)
64
+ >>> schema = loader.load('./api.yaml')
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ http_client: httpx.Client | None = None,
70
+ resolve_external_refs: bool = False,
71
+ base_path: str | Path | None = None,
72
+ ):
73
+ """Initialize the schema loader.
74
+
75
+ Args:
76
+ http_client: Optional HTTP client to use for URL requests.
77
+ If not provided, a default client will be created.
78
+ resolve_external_refs: Whether to resolve external $ref references.
79
+ If True, external URLs and relative file paths
80
+ will be loaded and inlined.
81
+ base_path: Base path for resolving relative file references.
82
+ Defaults to current working directory.
83
+ """
84
+ self._http_client = http_client
85
+ self._resolve_external_refs = resolve_external_refs
86
+ self._base_path = Path(base_path) if base_path else Path.cwd()
87
+ self._external_cache: dict[str, dict] = {}
88
+ self._upgrade_warnings: list[str] = []
89
+
90
+ def load(self, source: str) -> OpenAPIv3:
91
+ """Load and validate an OpenAPI schema from a URL or file path.
92
+
93
+ This method:
94
+ 1. Loads the content from the source (URL or file)
95
+ 2. Parses JSON or YAML content
96
+ 3. Detects the OpenAPI version
97
+ 4. Optionally resolves external $ref references
98
+ 5. Upgrades older versions to OpenAPI 3.2
99
+ 6. Validates the final schema
100
+
101
+ Args:
102
+ source: URL or file path to the OpenAPI schema.
103
+
104
+ Returns:
105
+ Validated OpenAPIv3 (3.2) object.
106
+
107
+ Raises:
108
+ SchemaLoadError: If the schema cannot be loaded from the source.
109
+ SchemaValidationError: If the schema is not valid OpenAPI.
110
+ """
111
+ try:
112
+ if self._is_url(source):
113
+ content = self._load_from_url(source)
114
+ self._current_base_url = source.rsplit('/', 1)[0] + '/'
115
+ else:
116
+ content = self._load_from_file(source)
117
+ self._current_base_url = None
118
+ source_path = Path(source)
119
+ if source_path.is_absolute():
120
+ self._base_path = source_path.parent
121
+ else:
122
+ self._base_path = (Path.cwd() / source_path).parent
123
+
124
+ if self._resolve_external_refs:
125
+ content = self._resolve_refs(content, source)
126
+
127
+ return self._validate_and_upgrade(content, source)
128
+
129
+ except SchemaLoadError:
130
+ raise
131
+ except SchemaValidationError:
132
+ raise
133
+ except Exception as e:
134
+ raise SchemaLoadError(source, cause=e)
135
+
136
+ def get_upgrade_warnings(self) -> list[str]:
137
+ """Get any warnings generated during schema upgrade."""
138
+ return self._upgrade_warnings.copy()
139
+
140
+ def get_detected_version(self, content: dict) -> str:
141
+ """Detect the OpenAPI/Swagger version from schema content."""
142
+ if 'swagger' in content:
143
+ return content.get('swagger', '2.0')
144
+ openapi_version = content.get('openapi', '3.0.0')
145
+ if openapi_version.startswith('3.0'):
146
+ return '3.0'
147
+ elif openapi_version.startswith('3.1'):
148
+ return '3.1'
149
+ elif openapi_version.startswith('3.2'):
150
+ return '3.2'
151
+ return openapi_version
152
+
153
+ def _is_url(self, text: str) -> bool:
154
+ """Check if a string is a URL."""
155
+ try:
156
+ result = urlparse(text)
157
+ return result.scheme in ('http', 'https')
158
+ except ValueError:
159
+ return False
160
+
161
+ def _load_from_url(self, url: str) -> dict:
162
+ """Load schema content from a URL."""
163
+ try:
164
+ if self._http_client:
165
+ response = self._http_client.get(url)
166
+ else:
167
+ response = httpx.get(url, follow_redirects=True, timeout=30.0)
168
+
169
+ response.raise_for_status()
170
+ content_type = response.headers.get('content-type', '')
171
+ content = response.text
172
+
173
+ if 'yaml' in content_type or url.endswith(('.yaml', '.yml')):
174
+ return yaml.safe_load(content)
175
+ else:
176
+ return json.loads(content)
177
+
178
+ except httpx.HTTPError as e:
179
+ raise SchemaLoadError(url, cause=e)
180
+ except (json.JSONDecodeError, yaml.YAMLError) as e:
181
+ raise SchemaLoadError(url, cause=e)
182
+
183
+ def _load_from_file(self, file_path: str) -> dict:
184
+ """Load schema content from a file."""
185
+ path = Path(file_path)
186
+ if not path.is_absolute():
187
+ path = self._base_path / path
188
+
189
+ if not path.exists():
190
+ raise SchemaLoadError(
191
+ str(file_path), cause=FileNotFoundError(f'File not found: {path}')
192
+ )
193
+
194
+ try:
195
+ content = path.read_text(encoding='utf-8')
196
+ if path.suffix.lower() in ('.yaml', '.yml'):
197
+ return yaml.safe_load(content)
198
+ else:
199
+ return json.loads(content)
200
+ except (json.JSONDecodeError, yaml.YAMLError) as e:
201
+ raise SchemaLoadError(str(file_path), cause=e)
202
+ except OSError as e:
203
+ raise SchemaLoadError(str(file_path), cause=e)
204
+
205
+ def _resolve_refs(self, content: dict, source: str) -> dict:
206
+ """Resolve all external $ref references in the schema."""
207
+ return self._resolve_refs_recursive(content, source, set())
208
+
209
+ def _resolve_refs_recursive(self, obj: Any, base: str, visited: set[str]) -> Any:
210
+ """Recursively resolve external references."""
211
+ if isinstance(obj, dict):
212
+ if '$ref' in obj:
213
+ ref = obj['$ref']
214
+ if not ref.startswith('#'):
215
+ return self._resolve_external_ref(ref, base, visited)
216
+ return {
217
+ k: self._resolve_refs_recursive(v, base, visited)
218
+ for k, v in obj.items()
219
+ }
220
+ elif isinstance(obj, list):
221
+ return [self._resolve_refs_recursive(item, base, visited) for item in obj]
222
+ return obj
223
+
224
+ def _resolve_external_ref(self, ref: str, base: str, visited: set[str]) -> dict:
225
+ """Resolve an external $ref reference."""
226
+ if '#' in ref:
227
+ file_part, pointer = ref.split('#', 1)
228
+ else:
229
+ file_part, pointer = ref, ''
230
+
231
+ if self._is_url(file_part):
232
+ location = file_part
233
+ elif self._is_url(base):
234
+ location = urljoin(base, file_part)
235
+ else:
236
+ base_path = Path(base).parent if not Path(base).is_dir() else Path(base)
237
+ location = str(base_path / file_part)
238
+
239
+ cache_key = f'{location}#{pointer}'
240
+ if cache_key in visited:
241
+ logger.warning(f'Circular reference detected: {cache_key}')
242
+ return {'$ref': ref}
243
+
244
+ visited = visited | {cache_key}
245
+
246
+ if location not in self._external_cache:
247
+ try:
248
+ if self._is_url(location):
249
+ self._external_cache[location] = self._load_from_url(location)
250
+ else:
251
+ self._external_cache[location] = self._load_from_file(location)
252
+ except SchemaLoadError:
253
+ logger.warning(f'Failed to resolve external reference: {ref}')
254
+ return {'$ref': ref}
255
+
256
+ content = self._external_cache[location]
257
+
258
+ if pointer:
259
+ content = self._resolve_json_pointer(content, pointer)
260
+
261
+ return self._resolve_refs_recursive(content, location, visited)
262
+
263
+ def _resolve_json_pointer(self, obj: Any, pointer: str) -> Any:
264
+ """Resolve a JSON pointer within an object."""
265
+ if not pointer or pointer == '/':
266
+ return obj
267
+
268
+ parts = pointer.strip('/').split('/')
269
+ current = obj
270
+
271
+ for part in parts:
272
+ part = part.replace('~1', '/').replace('~0', '~')
273
+ if isinstance(current, dict):
274
+ if part not in current:
275
+ raise ValueError(f'JSON pointer path not found: {pointer}')
276
+ current = current[part]
277
+ elif isinstance(current, list):
278
+ try:
279
+ index = int(part)
280
+ current = current[index]
281
+ except (ValueError, IndexError):
282
+ raise ValueError(f'JSON pointer path not found: {pointer}')
283
+ else:
284
+ raise ValueError(f'JSON pointer path not found: {pointer}')
285
+
286
+ return current
287
+
288
+ def _validate_and_upgrade(self, content: dict, source: str) -> OpenAPIv3:
289
+ """Validate and upgrade schema content to OpenAPI 3.2."""
290
+ self._upgrade_warnings = []
291
+
292
+ try:
293
+ universal: UniversalOpenAPI = TypeAdapter(UniversalOpenAPI).validate_python(
294
+ content
295
+ )
296
+ schema = universal.root
297
+
298
+ if isinstance(schema, OpenAPIv3):
299
+ return schema
300
+
301
+ while not isinstance(schema, OpenAPIv3):
302
+ schema, warnings = schema.upgrade()
303
+ self._upgrade_warnings.extend(warnings)
304
+
305
+ return schema
306
+
307
+ except Exception as e:
308
+ raise SchemaValidationError(source, errors=[str(e)])
309
+
310
+
311
+ # =============================================================================
312
+ # Schema Resolver
313
+ # =============================================================================
314
+
315
+
316
+ class SchemaResolver:
317
+ """Resolves $ref references and manages schema lookups in OpenAPI documents.
318
+
319
+ This class provides a centralized way to resolve JSON References ($ref) in
320
+ OpenAPI documents, supporting both local references (within the same document)
321
+ and caching resolved references for performance.
322
+
323
+ Example:
324
+ >>> resolver = SchemaResolver(openapi_doc)
325
+ >>> schema, name = resolver.resolve_reference(ref)
326
+ >>> all_schemas = resolver.get_all_schemas()
327
+ """
328
+
329
+ def __init__(self, openapi: OpenAPIv3):
330
+ """Initialize the schema resolver.
331
+
332
+ Args:
333
+ openapi: The OpenAPI document to resolve references from.
334
+ """
335
+ self.openapi = openapi
336
+ self._cache: dict[str, tuple[Schema, str]] = {}
337
+
338
+ def resolve_reference(self, reference: Reference | Schema) -> tuple[Schema, str]:
339
+ """Resolve a $ref reference to its schema and name.
340
+
341
+ If the input is already a Schema (not a Reference), it is returned as-is
342
+ with its title as the name (if available).
343
+
344
+ Args:
345
+ reference: A Reference or Schema object to resolve.
346
+
347
+ Returns:
348
+ Tuple of (resolved Schema, schema name).
349
+
350
+ Raises:
351
+ SchemaReferenceError: If the reference cannot be resolved.
352
+ """
353
+ if isinstance(reference, Schema):
354
+ name = (
355
+ reference.title
356
+ if hasattr(reference, 'title') and reference.title
357
+ else None
358
+ )
359
+ return reference, self._sanitize_name(name) if name else None
360
+
361
+ ref_str = reference.ref
362
+
363
+ # Check cache first
364
+ if ref_str in self._cache:
365
+ return self._cache[ref_str]
366
+
367
+ # Resolve the reference
368
+ schema, name = self._resolve_ref_string(ref_str)
369
+
370
+ # Cache the result
371
+ self._cache[ref_str] = (schema, name)
372
+
373
+ return schema, name
374
+
375
+ def _resolve_ref_string(self, ref: str) -> tuple[Schema, str]:
376
+ """Resolve a $ref string to its schema.
377
+
378
+ Args:
379
+ ref: The $ref string (e.g., '#/components/schemas/Pet').
380
+
381
+ Returns:
382
+ Tuple of (resolved Schema, schema name).
383
+
384
+ Raises:
385
+ SchemaReferenceError: If the reference format is unsupported or
386
+ the referenced schema doesn't exist.
387
+ """
388
+ # Handle local references
389
+ if ref.startswith('#/'):
390
+ return self._resolve_local_reference(ref)
391
+
392
+ # Handle external references (not yet supported)
393
+ if ref.startswith('http://') or ref.startswith('https://'):
394
+ raise SchemaReferenceError(
395
+ ref,
396
+ 'External URL references are not yet supported. '
397
+ 'Consider inlining the referenced schema.',
398
+ )
399
+
400
+ if ref.startswith('./') or ref.startswith('../'):
401
+ raise SchemaReferenceError(
402
+ ref,
403
+ 'Relative file references are not yet supported. '
404
+ 'Consider using a tool to bundle your OpenAPI spec.',
405
+ )
406
+
407
+ raise SchemaReferenceError(ref, 'Unknown reference format')
408
+
409
+ def _resolve_local_reference(self, ref: str) -> tuple[Schema, str]:
410
+ """Resolve a local JSON Pointer reference.
411
+
412
+ Args:
413
+ ref: The local reference (e.g., '#/components/schemas/Pet').
414
+
415
+ Returns:
416
+ Tuple of (resolved Schema, schema name).
417
+
418
+ Raises:
419
+ SchemaReferenceError: If the reference path is invalid or
420
+ the schema doesn't exist.
421
+ """
422
+ # Parse the JSON Pointer
423
+ if not ref.startswith('#/'):
424
+ raise SchemaReferenceError(ref, 'Local reference must start with #/')
425
+
426
+ path_parts = ref[2:].split('/')
427
+
428
+ # Currently only support components/schemas references
429
+ if (
430
+ len(path_parts) >= 3
431
+ and path_parts[0] == 'components'
432
+ and path_parts[1] == 'schemas'
433
+ ):
434
+ schema_name = path_parts[2]
435
+ return self._get_component_schema(schema_name, ref)
436
+
437
+ # Support components/parameters references
438
+ if (
439
+ len(path_parts) >= 3
440
+ and path_parts[0] == 'components'
441
+ and path_parts[1] == 'parameters'
442
+ ):
443
+ raise SchemaReferenceError(
444
+ ref,
445
+ 'Parameter references should be resolved through the parameter resolver',
446
+ )
447
+
448
+ # Support components/responses references
449
+ if (
450
+ len(path_parts) >= 3
451
+ and path_parts[0] == 'components'
452
+ and path_parts[1] == 'responses'
453
+ ):
454
+ raise SchemaReferenceError(
455
+ ref,
456
+ 'Response references should be resolved through the response resolver',
457
+ )
458
+
459
+ raise SchemaReferenceError(
460
+ ref,
461
+ 'Unsupported reference path. Only #/components/schemas/... is currently supported.',
462
+ )
463
+
464
+ def _get_component_schema(self, schema_name: str, ref: str) -> tuple[Schema, str]:
465
+ """Get a schema from the components/schemas section.
466
+
467
+ Args:
468
+ schema_name: The name of the schema in components/schemas.
469
+ ref: The original reference string (for error messages).
470
+
471
+ Returns:
472
+ Tuple of (Schema, sanitized schema name).
473
+
474
+ Raises:
475
+ SchemaReferenceError: If the schema doesn't exist.
476
+ """
477
+ if not self.openapi.components:
478
+ raise SchemaReferenceError(
479
+ ref,
480
+ 'Document has no components section',
481
+ )
482
+
483
+ if not self.openapi.components.schemas:
484
+ raise SchemaReferenceError(
485
+ ref,
486
+ 'Document has no schemas in components',
487
+ )
488
+
489
+ schemas = self.openapi.components.schemas
490
+
491
+ if schema_name not in schemas:
492
+ available = ', '.join(sorted(schemas.keys())[:10])
493
+ if len(schemas) > 10:
494
+ available += f', ... ({len(schemas)} total)'
495
+ raise SchemaReferenceError(
496
+ ref,
497
+ f"Schema '{schema_name}' not found. Available schemas: {available}",
498
+ )
499
+
500
+ return schemas[schema_name], self._sanitize_name(schema_name)
501
+
502
+ def get_all_schemas(self) -> dict[str, Schema]:
503
+ """Get all schemas defined in the components/schemas section.
504
+
505
+ Returns:
506
+ Dictionary mapping schema names to Schema objects.
507
+ Returns an empty dict if no schemas are defined.
508
+ """
509
+ if not self.openapi.components or not self.openapi.components.schemas:
510
+ return {}
511
+ return dict(self.openapi.components.schemas)
512
+
513
+ def get_schema_names(self) -> list[str]:
514
+ """Get the names of all schemas in the document.
515
+
516
+ Returns:
517
+ List of schema names, sorted alphabetically.
518
+ """
519
+ return sorted(self.get_all_schemas().keys())
520
+
521
+ def has_schema(self, name: str) -> bool:
522
+ """Check if a schema exists in the document.
523
+
524
+ Args:
525
+ name: The schema name to check.
526
+
527
+ Returns:
528
+ True if the schema exists, False otherwise.
529
+ """
530
+ return name in self.get_all_schemas()
531
+
532
+ def clear_cache(self) -> None:
533
+ """Clear the reference resolution cache."""
534
+ self._cache.clear()
535
+
536
+ @staticmethod
537
+ def _sanitize_name(name: str | None) -> str | None:
538
+ """Sanitize a name to be a valid Python identifier.
539
+
540
+ Args:
541
+ name: The name to sanitize.
542
+
543
+ Returns:
544
+ Sanitized name, or None if input is None.
545
+ """
546
+ if name is None:
547
+ return None
548
+
549
+ # Import here to avoid circular imports
550
+ from otterapi.codegen.utils import sanitize_identifier
551
+
552
+ return sanitize_identifier(name)
553
+
554
+ def resolve_all_refs_in_schema(self, schema: Schema | Reference) -> Schema:
555
+ """Recursively resolve all $ref references in a schema.
556
+
557
+ This is useful when you need a fully resolved schema without
558
+ any remaining references.
559
+
560
+ Args:
561
+ schema: The schema (potentially containing references) to resolve.
562
+
563
+ Returns:
564
+ The resolved schema with all references expanded.
565
+
566
+ Note:
567
+ This does not modify the original schema; references are
568
+ resolved on access. For deeply nested schemas, this may
569
+ result in multiple resolutions of the same reference.
570
+ """
571
+ if isinstance(schema, Reference):
572
+ resolved, _ = self.resolve_reference(schema)
573
+ return resolved
574
+ return schema
575
+
576
+ def get_reference_target(self, ref: str) -> str:
577
+ """Extract the target name from a $ref string.
578
+
579
+ Args:
580
+ ref: The $ref string (e.g., '#/components/schemas/Pet').
581
+
582
+ Returns:
583
+ The target name (e.g., 'Pet').
584
+
585
+ Raises:
586
+ SchemaReferenceError: If the reference format is invalid.
587
+ """
588
+ if not ref.startswith('#/components/schemas/'):
589
+ raise SchemaReferenceError(
590
+ ref,
591
+ 'Can only extract target from #/components/schemas/ references',
592
+ )
593
+ return ref.split('/')[-1]