|
1 | 1 | from typing import Any, List, Optional, Union
|
| 2 | +from html import escape |
2 | 3 | from datetime import datetime
|
3 |
| -from ..base import BaseModel, Field |
| 4 | +from ..base import BaseModel, Field, register_renderer |
4 | 5 |
|
5 | 6 |
|
6 | 7 | class URL(BaseModel):
|
@@ -120,6 +121,7 @@ class ImageObject(BaseModel):
|
120 | 121 | Corresponds to the JSON-LD "ImageObject" type.
|
121 | 122 | https://schema.org/ImageObject
|
122 | 123 | """
|
| 124 | + name: str |
123 | 125 | url: str = Field(required=True)
|
124 | 126 | width: int = 0
|
125 | 127 | height: int = 0
|
@@ -362,3 +364,137 @@ class Audience(BaseModel):
|
362 | 364 |
|
363 | 365 | class Meta:
|
364 | 366 | schema_type: str = "Audience"
|
| 367 | + |
| 368 | + |
| 369 | +class Place(BaseModel): |
| 370 | + address: PostalAddress |
| 371 | + |
| 372 | + class Meta: |
| 373 | + schema_type = "Place" |
| 374 | + |
| 375 | +class QuantitativeValue(BaseModel): |
| 376 | + value: float |
| 377 | + unitText: str |
| 378 | + |
| 379 | + class Meta: |
| 380 | + schema_type = "QuantitativeValue" |
| 381 | + |
| 382 | +class MonetaryAmount(BaseModel): |
| 383 | + currency: Union[str, float] |
| 384 | + value: QuantitativeValue |
| 385 | + |
| 386 | + class Meta: |
| 387 | + schema_type = "MonetaryAmount" |
| 388 | + |
| 389 | +class JobPosting(BaseModel): |
| 390 | + title: str |
| 391 | + description: str |
| 392 | + hiringOrganization: Organization |
| 393 | + datePosted: str |
| 394 | + validThrough: str |
| 395 | + jobLocation: Optional[Place] |
| 396 | + baseSalary: Optional[MonetaryAmount] |
| 397 | + qualifications: str |
| 398 | + skills: List[str] |
| 399 | + responsibilities: str |
| 400 | + educationRequirements: str |
| 401 | + experienceRequirements: str |
| 402 | + |
| 403 | + class Meta: |
| 404 | + schema_type = "JobPosting" |
| 405 | + |
| 406 | + |
| 407 | +@register_renderer("ImageObject") |
| 408 | +def render_imageobject(model: "BaseModel", top_level: bool) -> str: |
| 409 | + """ |
| 410 | + Render an ImageObject in a custom way: |
| 411 | + <div typeof="ImageObject"> |
| 412 | + <h2 property="name">...</h2> |
| 413 | + <img src="..." alt="..." property="contentUrl"/> |
| 414 | + ... |
| 415 | + </div> |
| 416 | + """ |
| 417 | + # You can read fields from the model (like name, url, caption, etc.). |
| 418 | + # If you used 'url' as the field that is the image's src, we can do: |
| 419 | + name = getattr(model, "name", None) # or None if not present |
| 420 | + url = getattr(model, "url", None) |
| 421 | + caption = getattr(model, "caption", None) |
| 422 | + width = getattr(model, "width", None) |
| 423 | + height = getattr(model, "height", None) |
| 424 | + |
| 425 | + schema_type = getattr(model.Meta, 'schema_type', model.__class__.__name__) |
| 426 | + |
| 427 | + container_open = "" |
| 428 | + if top_level: |
| 429 | + container_open = f'<div vocab="https://schema.org/" typeof="{escape(schema_type)}">' # noqa |
| 430 | + else: |
| 431 | + # when nested, we might do property="image" or property=schema_type |
| 432 | + container_open = f'<div property="{escape(schema_type)}" typeof="{escape(schema_type)}">' # noqa |
| 433 | + |
| 434 | + pieces = [container_open] |
| 435 | + |
| 436 | + # Render name as <h2 property="name">Name</h2> if present |
| 437 | + if name: |
| 438 | + name_esc = escape(str(name)) |
| 439 | + pieces.append(f'<h2 property="name">{name_esc}</h2>') |
| 440 | + |
| 441 | + # Now create an <img> that has property="contentUrl" or "url" |
| 442 | + if url: |
| 443 | + url_esc = escape(str(url)) |
| 444 | + caption_esc = escape(str(caption or "")) |
| 445 | + # alt can come from caption |
| 446 | + snippet = f'<img property="contentUrl" src="{url_esc}" alt="{caption_esc}"' |
| 447 | + if width: |
| 448 | + snippet += f' width="{escape(str(width))}"' |
| 449 | + if height: |
| 450 | + snippet += f' height="{escape(str(height))}"' |
| 451 | + snippet += ' />' |
| 452 | + pieces.append(snippet) |
| 453 | + |
| 454 | + # If we have other fields not individually handled, we could do a fallback |
| 455 | + # to the normal field iteration. But for brevity, let's omit that here. |
| 456 | + # E.g. leftover = model.render_remaining_fields(…) |
| 457 | + # pieces.append(leftover) |
| 458 | + |
| 459 | + pieces.append('</div>') |
| 460 | + return "\n".join(pieces) |
| 461 | + |
| 462 | + |
| 463 | +@register_renderer("GeoCoordinates") |
| 464 | +def render_geocoordinates(model: "BaseModel", top_level: bool) -> str: |
| 465 | + """ |
| 466 | + Custom renderer for GeoCoordinates: |
| 467 | + <div property="geo" typeof="GeoCoordinates"> |
| 468 | + <meta property="latitude" content="40.75"/> |
| 469 | + <meta property="longitude" content="-73.98"/> |
| 470 | + ... |
| 471 | + </div> |
| 472 | + """ |
| 473 | + lat = getattr(model, "latitude", None) |
| 474 | + lng = getattr(model, "longitude", None) |
| 475 | + elevation = getattr(model, "elevation", None) |
| 476 | + |
| 477 | + schema_type = getattr(model.Meta, 'schema_type', model.__class__.__name__) |
| 478 | + if top_level: |
| 479 | + container_open = f'<div vocab="https://schema.org/" typeof="{escape(schema_type)}">' # noqa |
| 480 | + else: |
| 481 | + container_open = f'<div property="{escape(schema_type)}" typeof="{escape(schema_type)}">' # noqa |
| 482 | + |
| 483 | + pieces = [container_open] |
| 484 | + |
| 485 | + # For numeric fields, we might prefer <meta ... content="..."/> |
| 486 | + if lat is not None: |
| 487 | + pieces.append( |
| 488 | + f'<meta property="latitude" content="{escape(str(lat))}" />' |
| 489 | + ) |
| 490 | + if lng is not None: |
| 491 | + pieces.append( |
| 492 | + f'<meta property="longitude" content="{escape(str(lng))}" />' |
| 493 | + ) |
| 494 | + if elevation is not None: |
| 495 | + pieces.append( |
| 496 | + f'<meta property="elevation" content="{escape(str(elevation))}" />' |
| 497 | + ) |
| 498 | + |
| 499 | + pieces.append('</div>') |
| 500 | + return "\n".join(pieces) |
0 commit comments