dyff-schema 0.25.1__tar.gz → 0.26.0__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.

Potentially problematic release.


This version of dyff-schema might be problematic. Click here for more details.

Files changed (62) hide show
  1. {dyff_schema-0.25.1/dyff_schema.egg-info → dyff_schema-0.26.0}/PKG-INFO +1 -1
  2. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/base.py +20 -0
  3. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/commands.py +103 -8
  4. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/platform.py +34 -23
  5. {dyff_schema-0.25.1 → dyff_schema-0.26.0/dyff_schema.egg-info}/PKG-INFO +1 -1
  6. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/.gitignore +0 -0
  7. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/.gitlab-ci.yml +0 -0
  8. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/.licenserc.yaml +0 -0
  9. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/.pre-commit-config.yaml +0 -0
  10. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/.prettierignore +0 -0
  11. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/.secrets.baseline +0 -0
  12. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/CODE_OF_CONDUCT.md +0 -0
  13. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/LICENSE +0 -0
  14. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/NOTICE +0 -0
  15. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/README.md +0 -0
  16. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/__init__.py +0 -0
  17. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/adapters.py +0 -0
  18. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/annotations.py +0 -0
  19. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/base.py +0 -0
  20. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/commands.py +0 -0
  21. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/copydoc.py +0 -0
  22. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/dataset/__init__.py +0 -0
  23. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/dataset/arrow.py +0 -0
  24. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/dataset/binary.py +0 -0
  25. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/dataset/classification.py +0 -0
  26. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/dataset/embedding.py +0 -0
  27. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/dataset/text.py +0 -0
  28. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/dataset/vision.py +0 -0
  29. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/errors.py +0 -0
  30. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/ids.py +0 -0
  31. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/io/__init__.py +0 -0
  32. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/io/vllm.py +0 -0
  33. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/platform.py +0 -0
  34. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/py.typed +0 -0
  35. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/quantity.py +0 -0
  36. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/requests.py +0 -0
  37. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/test.py +0 -0
  38. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/__init__.py +0 -0
  39. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/__init__.py +0 -0
  40. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/adapters.py +0 -0
  41. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/dataset/__init__.py +0 -0
  42. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/dataset/arrow.py +0 -0
  43. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/dataset/binary.py +0 -0
  44. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/dataset/classification.py +0 -0
  45. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/dataset/embedding.py +0 -0
  46. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/dataset/text.py +0 -0
  47. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/dataset/vision.py +0 -0
  48. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/io/__init__.py +0 -0
  49. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/io/vllm.py +0 -0
  50. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/requests.py +0 -0
  51. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/test.py +0 -0
  52. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/v0/r1/version.py +0 -0
  53. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff/schema/version.py +0 -0
  54. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff_schema.egg-info/SOURCES.txt +0 -0
  55. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff_schema.egg-info/dependency_links.txt +0 -0
  56. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff_schema.egg-info/requires.txt +0 -0
  57. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/dyff_schema.egg-info/top_level.txt +0 -0
  58. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/makefile +0 -0
  59. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/pyproject.toml +0 -0
  60. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/setup.cfg +0 -0
  61. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/tests/test_adapters.py +0 -0
  62. {dyff_schema-0.25.1 → dyff_schema-0.26.0}/tests/test_import.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dyff-schema
3
- Version: 0.25.1
3
+ Version: 0.26.0
4
4
  Summary: Data models for the Dyff AI auditing platform.
5
5
  Author-email: Digital Safety Research Institute <contact@dsri.org>
6
6
  License: Apache-2.0
@@ -561,6 +561,25 @@ def list_(
561
561
  return pydantic.conlist(item_type, min_items=list_size, max_items=list_size)
562
562
 
563
563
 
564
+ class Null:
565
+ """Use this type in a Union to make Pydantic generate a JSON Schema that accepts
566
+ 'null' for the field value."""
567
+
568
+ @classmethod
569
+ def __get_validators__(cls): # -> Generator[Callable, None, None]:
570
+ yield cls.validate
571
+
572
+ @classmethod
573
+ def validate(cls, value: Any, field: pydantic.fields.ModelField) -> None:
574
+ if value is not None:
575
+ raise ValueError()
576
+ return None
577
+
578
+ @classmethod
579
+ def __modify_schema__(cls, field_schema: dict[str, Any]) -> None:
580
+ field_schema["type"] = "null"
581
+
582
+
564
583
  # mypy gets confused because 'dict' is the name of a method in DyffBaseModel
565
584
  _ModelAsDict = dict[str, Any]
566
585
 
@@ -649,6 +668,7 @@ __all__ = [
649
668
  "Int16",
650
669
  "Int32",
651
670
  "Int64",
671
+ "Null",
652
672
  "UInt8",
653
673
  "UInt16",
654
674
  "UInt32",
@@ -7,19 +7,23 @@ These are used internally by the platform and users typically won't encounter th
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import Any, Literal, Union
10
+ from typing import Any, Literal, Optional, Union
11
11
 
12
12
  import pydantic
13
13
 
14
- from .base import DyffSchemaBaseModel
14
+ from .base import DyffSchemaBaseModel, Null
15
15
  from .platform import (
16
- Documented,
17
16
  DyffEntityType,
18
17
  EntityKindLiteral,
18
+ FamilyMember,
19
19
  FamilyMembers,
20
- Labeled,
20
+ LabelKeyType,
21
+ LabelValueType,
21
22
  SchemaVersion,
22
23
  Status,
24
+ TagNameType,
25
+ summary_maxlen,
26
+ title_maxlen,
23
27
  )
24
28
 
25
29
  # ----------------------------------------------------------------------------
@@ -54,7 +58,6 @@ class Command(SchemaVersion):
54
58
  """
55
59
 
56
60
  command: Literal[
57
- "AppendRevisionToHistory",
58
61
  "CreateEntity",
59
62
  "EditEntityDocumentation",
60
63
  "EditEntityLabels",
@@ -91,9 +94,50 @@ class CreateEntity(Command):
91
94
  # ----------------------------------------------------------------------------
92
95
 
93
96
 
94
- class EditEntityDocumentationData(Documented, EntityIdentifier):
97
+ class EditEntityDocumentationPatch(DyffSchemaBaseModel):
98
+ """Same properties as DocumentationBase, but generates a JSON Schema that allows
99
+ fields to be set to JSON 'null'.
100
+
101
+ This is needed to get JSON Merge Patch semantics, where explicit 'null' means
102
+ "delete that field".
103
+ """
104
+
105
+ title: Optional[Union[pydantic.constr(max_length=title_maxlen()), Null]] = ( # type: ignore
106
+ pydantic.Field(
107
+ default=None,
108
+ description='A short plain string suitable as a title or "headline".',
109
+ )
110
+ )
111
+
112
+ summary: Optional[Union[pydantic.constr(max_length=summary_maxlen()), Null]] = ( # type: ignore
113
+ pydantic.Field(
114
+ default=None,
115
+ description="A brief summary, suitable for display in"
116
+ " small UI elements. Interpreted as Markdown. Excessively long"
117
+ " summaries may be truncated in the UI, especially on small displays.",
118
+ )
119
+ )
120
+
121
+ fullPage: Optional[Union[str, Null]] = pydantic.Field(
122
+ default=None,
123
+ description="Long-form documentation. Interpreted as"
124
+ " Markdown. There are no length constraints, but be reasonable.",
125
+ )
126
+
127
+
128
+ class EditEntityDocumentationAttributes(DyffSchemaBaseModel):
129
+ documentation: EditEntityDocumentationPatch = pydantic.Field(
130
+ description="Edits to make to the documentation."
131
+ )
132
+
133
+
134
+ class EditEntityDocumentationData(EntityIdentifier):
95
135
  """Payload data for the EditEntityDocumentation command."""
96
136
 
137
+ attributes: EditEntityDocumentationAttributes = pydantic.Field(
138
+ description="The command attributes"
139
+ )
140
+
97
141
 
98
142
  class EditEntityDocumentation(Command):
99
143
  """Edit the documentation associated with an entity.
@@ -110,9 +154,29 @@ class EditEntityDocumentation(Command):
110
154
  # ----------------------------------------------------------------------------
111
155
 
112
156
 
113
- class EditEntityLabelsData(Labeled, EntityIdentifier):
157
+ class EditEntityLabelsAttributes(DyffSchemaBaseModel):
158
+ labels: dict[LabelKeyType, Optional[Union[LabelValueType, Null]]] = pydantic.Field(
159
+ default_factory=dict,
160
+ description="A set of key-value labels for the resource. Used to"
161
+ " specify identifying attributes of resources that are meaningful to"
162
+ " users but do not imply semantics in the dyff system.\n\n"
163
+ "The keys are DNS labels with an optional DNS domain prefix."
164
+ " For example: 'my-key', 'your.com/key_0'. Keys prefixed with"
165
+ " 'dyff.io/', 'subdomain.dyff.io/', etc. are reserved.\n\n"
166
+ "The label values are alphanumeric characters separated by"
167
+ " '.', '-', or '_'.\n\n"
168
+ "We follow the kubernetes label conventions closely."
169
+ " See: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels",
170
+ )
171
+
172
+
173
+ class EditEntityLabelsData(EntityIdentifier):
114
174
  """Payload data for the EditEntityLabels command."""
115
175
 
176
+ attributes: EditEntityLabelsAttributes = pydantic.Field(
177
+ description="The command attributes"
178
+ )
179
+
116
180
 
117
181
  class EditEntityLabels(Command):
118
182
  """Edit the labels associated with an entity.
@@ -129,9 +193,21 @@ class EditEntityLabels(Command):
129
193
  # ----------------------------------------------------------------------------
130
194
 
131
195
 
196
+ class EditFamilyMembersAttributes(DyffSchemaBaseModel):
197
+ """Payload data for the EditFamilyMembers command."""
198
+
199
+ members: dict[TagNameType, Optional[Union[FamilyMember, Null]]] = pydantic.Field(
200
+ description="Mapping of names to IDs of member resources.",
201
+ )
202
+
203
+
132
204
  class EditFamilyMembersData(FamilyMembers, FamilyIdentifier):
133
205
  """Payload data for the EditFamilyMembers command."""
134
206
 
207
+ attributes: EditFamilyMembersAttributes = pydantic.Field(
208
+ description="The command attributes"
209
+ )
210
+
135
211
 
136
212
  class EditFamilyMembers(Command):
137
213
  """Edit the labels associated with an entity.
@@ -159,9 +235,23 @@ class ForgetEntity(Command):
159
235
  # ----------------------------------------------------------------------------
160
236
 
161
237
 
162
- class UpdateEntityStatusData(Status, EntityIdentifier):
238
+ class UpdateEntityStatusAttributes(DyffSchemaBaseModel):
239
+ status: str = pydantic.Field(
240
+ description=Status.__fields__["status"].field_info.description
241
+ )
242
+
243
+ reason: Union[str, Null] = pydantic.Field(
244
+ description=Status.__fields__["reason"].field_info.description
245
+ )
246
+
247
+
248
+ class UpdateEntityStatusData(EntityIdentifier):
163
249
  """Payload data for the UpdateEntityStatus command."""
164
250
 
251
+ attributes: UpdateEntityStatusAttributes = pydantic.Field(
252
+ description="The command attributes"
253
+ )
254
+
165
255
 
166
256
  class UpdateEntityStatus(Command):
167
257
  """Update the status fields of an entity."""
@@ -189,14 +279,19 @@ __all__ = [
189
279
  "CreateEntity",
190
280
  "DyffCommandType",
191
281
  "EditEntityDocumentation",
282
+ "EditEntityDocumentationAttributes",
192
283
  "EditEntityDocumentationData",
284
+ "EditEntityDocumentationPatch",
193
285
  "EditEntityLabels",
286
+ "EditEntityLabelsAttributes",
194
287
  "EditEntityLabelsData",
195
288
  "EditFamilyMembers",
289
+ "EditFamilyMembersAttributes",
196
290
  "EditFamilyMembersData",
197
291
  "EntityIdentifier",
198
292
  "FamilyIdentifier",
199
293
  "ForgetEntity",
200
294
  "UpdateEntityStatus",
295
+ "UpdateEntityStatusAttributes",
201
296
  "UpdateEntityStatusData",
202
297
  ]
@@ -291,21 +291,29 @@ class DyffModelWithID(DyffSchemaBaseModel):
291
291
  account: str = pydantic.Field(description="Account that owns the entity")
292
292
 
293
293
 
294
- LabelKey: TypeAlias = pydantic.constr( # type: ignore
295
- regex=_k8s_label_key_regex(), max_length=_k8s_label_key_maxlen()
296
- )
294
+ def LabelKey() -> type[str]:
295
+ return pydantic.constr(
296
+ regex=_k8s_label_key_regex(), max_length=_k8s_label_key_maxlen()
297
+ )
297
298
 
298
299
 
299
- LabelValue: TypeAlias = Optional[ # type: ignore
300
- pydantic.constr( # type: ignore
300
+ def LabelValue() -> type[str]:
301
+ return pydantic.constr( # type: ignore
301
302
  regex=_k8s_label_value_regex(), max_length=_k8s_label_value_maxlen()
302
303
  )
303
- ]
304
304
 
305
305
 
306
- TagName: TypeAlias = pydantic.constr( # type: ignore
307
- regex=_oci_image_tag_regex(), max_length=_k8s_label_key_maxlen()
308
- )
306
+ def TagName() -> type[str]:
307
+ return pydantic.constr( # type: ignore
308
+ regex=_oci_image_tag_regex(), max_length=_k8s_label_key_maxlen()
309
+ )
310
+
311
+
312
+ LabelKeyType: TypeAlias = LabelKey() # type: ignore
313
+
314
+ LabelValueType: TypeAlias = LabelValue() # type: ignore
315
+
316
+ TagNameType: TypeAlias = TagName() # type: ignore
309
317
 
310
318
 
311
319
  class Label(DyffSchemaBaseModel):
@@ -317,20 +325,20 @@ class Label(DyffSchemaBaseModel):
317
325
  https://kubernetes.io/docs/concepts/overview/working-with-objects/labels
318
326
  """
319
327
 
320
- key: LabelKey = pydantic.Field(
328
+ key: LabelKeyType = pydantic.Field( # type: ignore
321
329
  description="The label key is a DNS label with an optional DNS domain"
322
330
  " prefix. For example: 'my-key', 'your.com/key_0'. Keys prefixed with"
323
331
  " 'dyff.io/', 'subdomain.dyff.io/', etc. are reserved.",
324
332
  )
325
333
 
326
- value: LabelValue = pydantic.Field(
334
+ value: LabelValueType = pydantic.Field( # type: ignore
327
335
  description="The label value consists of alphanumeric characters"
328
336
  " separated by '.', '-', or '_'.",
329
337
  )
330
338
 
331
339
 
332
340
  class Labeled(DyffSchemaBaseModel):
333
- labels: dict[LabelKey, LabelValue] = pydantic.Field(
341
+ labels: dict[LabelKeyType, LabelValueType] = pydantic.Field( # type: ignore
334
342
  default_factory=dict,
335
343
  description="A set of key-value labels for the resource. Used to"
336
344
  " specify identifying attributes of resources that are meaningful to"
@@ -372,7 +380,7 @@ class ServiceClass(str, enum.Enum):
372
380
 
373
381
 
374
382
  class ResourceAllocation(DyffSchemaBaseModel):
375
- quantities: dict[LabelKey, Quantity] = pydantic.Field(
383
+ quantities: dict[LabelKeyType, Quantity] = pydantic.Field( # type: ignore
376
384
  default_factory=dict,
377
385
  description="Mapping of resource keys to quantities to be allocated.",
378
386
  )
@@ -382,7 +390,7 @@ class ResourceAllocation(DyffSchemaBaseModel):
382
390
 
383
391
  class Status(DyffSchemaBaseModel):
384
392
  status: str = pydantic.Field(
385
- default=None, description="Top-level resource status (assigned by system)"
393
+ description="Top-level resource status (assigned by system)"
386
394
  )
387
395
 
388
396
  reason: Optional[str] = pydantic.Field(
@@ -441,6 +449,12 @@ class DyffEntityMetadata(DyffSchemaBaseModel):
441
449
  description="Unique identifier of the current revision of the entity.",
442
450
  )
443
451
 
452
+ documentation: Optional[DocumentationBase] = pydantic.Field(
453
+ default_factory=DocumentationBase,
454
+ description="Documentation of the resource. The content is used to"
455
+ " populate various views in the web UI.",
456
+ )
457
+
444
458
 
445
459
  class DyffEntity(Status, Labeled, SchemaVersion, DyffModelWithID):
446
460
  kind: Literal[
@@ -668,7 +682,7 @@ class FamilyMemberKind(str, enum.Enum):
668
682
 
669
683
 
670
684
  class FamilyMemberBase(DyffSchemaBaseModel):
671
- name: TagName = pydantic.Field(
685
+ name: TagNameType = pydantic.Field(
672
686
  description="An interpretable identifier for the member that is unique"
673
687
  " in the context of the corresponding Family.",
674
688
  )
@@ -696,7 +710,7 @@ class FamilyMember(FamilyMemberBase):
696
710
 
697
711
 
698
712
  class FamilyMembers(DyffSchemaBaseModel):
699
- members: dict[TagName, FamilyMember] = pydantic.Field(
713
+ members: dict[TagNameType, FamilyMember] = pydantic.Field(
700
714
  default_factory=dict,
701
715
  description="Mapping of names to IDs of member resources.",
702
716
  )
@@ -711,12 +725,6 @@ class FamilyBase(DyffSchemaBaseModel):
711
725
  class Family(DyffEntity, FamilyBase, FamilyMembers):
712
726
  kind: Literal["Family"] = "Family"
713
727
 
714
- documentation: DocumentationBase = pydantic.Field(
715
- default_factory=DocumentationBase,
716
- description="Documentation of the resource family. The content is used"
717
- " to populate various views in the web UI.",
718
- )
719
-
720
728
 
721
729
  class RevisionMetadata(DyffSchemaBaseModel):
722
730
  previousRevision: Optional[str] = pydantic.Field(
@@ -1207,7 +1215,7 @@ class ContainerImageSource(DyffSchemaBaseModel):
1207
1215
  " digest, even if 'tag' is specified.",
1208
1216
  regex=r"^sha256:[0-9a-f]{64}$",
1209
1217
  )
1210
- tag: Optional[TagName] = pydantic.Field(
1218
+ tag: Optional[TagNameType] = pydantic.Field(
1211
1219
  default=None,
1212
1220
  description="The tag of the image. Although the image is always pulled"
1213
1221
  " by digest, including the tag is strongly recommended as it is often"
@@ -2479,7 +2487,9 @@ __all__ = [
2479
2487
  "InferenceSessionSpec",
2480
2488
  "Label",
2481
2489
  "LabelKey",
2490
+ "LabelKeyType",
2482
2491
  "LabelValue",
2492
+ "LabelValueType",
2483
2493
  "Labeled",
2484
2494
  "Measurement",
2485
2495
  "MeasurementLevel",
@@ -2532,6 +2542,7 @@ __all__ = [
2532
2542
  "Status",
2533
2543
  "StorageSignedURL",
2534
2544
  "TagName",
2545
+ "TagNameType",
2535
2546
  "TaskSchema",
2536
2547
  "UseCase",
2537
2548
  "entity_class",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dyff-schema
3
- Version: 0.25.1
3
+ Version: 0.26.0
4
4
  Summary: Data models for the Dyff AI auditing platform.
5
5
  Author-email: Digital Safety Research Institute <contact@dsri.org>
6
6
  License: Apache-2.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes