pymetadataeditor 0.3.0__tar.gz → 0.3.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymetadataeditor
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Metadata Editor client for python
5
5
  License: MIT
6
6
  Author: Gordon Blackadder
@@ -11,8 +11,8 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Requires-Dist: markitdown (>=0.0.1a3,<0.0.2)
14
- Requires-Dist: metadataschemas (>=0.1.2,<0.2.0)
15
- Requires-Dist: openai (>=1.59.4,<2.0.0)
14
+ Requires-Dist: metadataschemas (>=0.1.3,<0.2.0)
15
+ Requires-Dist: openai (>=2.8.0,<3.0.0)
16
16
  Requires-Dist: pandas (>=2.2.2,<3.0.0)
17
17
  Requires-Dist: pydantic (>=2.7.4,<3.0.0)
18
18
  Requires-Dist: requests (>=2.32.3,<3.0.0)
@@ -427,12 +427,21 @@ class MetadataEditor:
427
427
  else:
428
428
  metadata_type = self._templates[template_uid]["metadata_type"]
429
429
  if "class" not in self._templates[template_uid] or apply_template_rules is False:
430
- parent_schema = self._mm.metadata_class_from_name(metadata_type)
430
+ try:
431
+ parent_schema = self._mm.metadata_class_from_name(metadata_type)
432
+ except ValueError:
433
+ warnings.warn(
434
+ f"Could not find a metadata class for template type '{metadata_type}', UID '{template_uid}'. "
435
+ f"Building a class from the template without a parent schema."
436
+ )
437
+ parent_schema = SchemaBaseModel
438
+
431
439
  klass = pydantic_from_template(
432
440
  temp.template,
433
441
  parent_schema=parent_schema,
434
442
  uid=template_uid,
435
443
  name=temp.name,
444
+ metadata_type=metadata_type,
436
445
  apply_rules=apply_template_rules,
437
446
  )
438
447
  if apply_template_rules:
@@ -693,7 +702,7 @@ class MetadataEditor:
693
702
  tokenizer_model="o200k_base",
694
703
  max_tokens=128_000,
695
704
  llm_base_url: Optional[str] = None,
696
- azure_llm_base_url: Optional[str] = None,
705
+ azure_deployment_name: Optional[str] = None,
697
706
  ) -> Union[BaseModel, Dict, str]:
698
707
  """Automatically generate *draft* metadata for a project based on local files or web pages.
699
708
 
@@ -730,11 +739,13 @@ class MetadataEditor:
730
739
  max_tokens (int): The maximum number of tokens to use when sending the content to OpenAI.
731
740
  Defaults to 128_000, which has been the typical maximum for the 4o model.
732
741
  llm_base_url (Optional[str]): The base URL for the LLM API. If None, the default URL is used which
733
- sends the request to OpenAI. This argument is ignored if an azure_llm_base_url is provided.
734
- azure_llm_base_url (Optional[str]): The base URL for the Azure LLM API. Typically used when an organization
735
- has its own deployment of an LLM model, possibly for privacy reasons. The Azure endpoint will be used
736
- even if a llm_base_url is provided. If None, then the llm_base_url endpoint is used. If
737
- that's also None, then OpenAI is used.
742
+ sends the request to OpenAI. Alternatively, this can be the base URL for a local LLM instance such as
743
+ Ollama or a private deployment of an LLM model. If using Azure OpenAI, this should be the Azure
744
+ endpoint URL (eg "https://my-azure-openai-resource.openai.azure.com/") and the azure_deployment_name
745
+ parameter should also be set.
746
+ azure_deployment_name (Optional[str]): Used when an organization has its own deployment of an LLM model in
747
+ Azure, possibly for privacy reasons. Be sure to provide the llm_base_url parameter as well which should
748
+ be the Azure endpoint URL.
738
749
 
739
750
  Returns:
740
751
  (Union[BaseModel, Dict, str]): If mode == 'dict', a dictionary is returned. If mode == 'pydantic', a
@@ -776,7 +787,8 @@ class MetadataEditor:
776
787
  metadata_producer_organization="My Organization",
777
788
  filename="output.xlsx",
778
789
  title="My Metadata",
779
- azure_llm_base_url="https://my-azure-openai-resource.openai.azure.com/",
790
+ llm_base_url="https://my-azure-openai-resource.openai.azure.com/",
791
+ azure_deployment_name="my-llm-deployment"
780
792
  )
781
793
  ```
782
794
  """
@@ -786,10 +798,15 @@ class MetadataEditor:
786
798
  metadata_type_or_template_uid, apply_template_rules=False
787
799
  )
788
800
  enc = tiktoken.get_encoding(tokenizer_model)
789
- if azure_llm_base_url is None:
801
+ if azure_deployment_name is None:
790
802
  client = OpenAI(api_key=llm_api_key, base_url=llm_base_url)
791
803
  else:
792
- client = AzureOpenAI(api_key=llm_api_key, base_url=azure_llm_base_url, api_version="2024-10-01")
804
+ client = AzureOpenAI(
805
+ api_key=llm_api_key,
806
+ azure_endpoint=llm_base_url,
807
+ azure_deployment=azure_deployment_name,
808
+ api_version="2024-10-01",
809
+ )
793
810
 
794
811
  system_prompt = f"You are an expert on producing {metadata_type} documentation. "
795
812
  system_prompt += (
@@ -829,9 +846,7 @@ class MetadataEditor:
829
846
  print(f"Reading {doc}, running token count is {num_tokens}")
830
847
  messages += user_message
831
848
 
832
- endpoint_name = (
833
- azure_llm_base_url if azure_llm_base_url is not None else "OpenAI" if llm_base_url is None else llm_base_url
834
- )
849
+ endpoint_name = llm_base_url if llm_base_url is not None else "OpenAI"
835
850
  print(f"Sending to {endpoint_name}, this may take a few minutes...")
836
851
  try:
837
852
  completion = client.beta.chat.completions.parse(
@@ -876,7 +891,7 @@ class MetadataEditor:
876
891
  tokenizer_model="o200k_base",
877
892
  max_tokens=128_000,
878
893
  llm_base_url: Optional[str] = None,
879
- azure_llm_base_url: Optional[str] = None,
894
+ azure_deployment_name: Optional[str] = None,
880
895
  ) -> Union[BaseModel, Dict, str]:
881
896
  """Augment existing metadata with information from files or web pages.
882
897
 
@@ -905,11 +920,13 @@ class MetadataEditor:
905
920
  max_tokens (int): The maximum number of tokens to use when sending the content to OpenAI.
906
921
  Defaults to 128_000, which has been the typical maximum for the 4o model.
907
922
  llm_base_url (Optional[str]): The base URL for the LLM API. If None, the default URL is used which
908
- sends the request to OpenAI.
909
- azure_llm_base_url (Optional[str]): The base URL for the Azure LLM API. Typically used when an organization
910
- has its own deployment of an LLM model, possibly for privacy reasons. The Azure endpoint will be used
911
- even if a llm_base_url is provided. If None, then the llm_base_url endpoint is used. If
912
- that's also None, then OpenAI is used.
923
+ sends the request to OpenAI. Alternatively, this can be the base URL for a local LLM instance such as
924
+ Ollama or a private deployment of an LLM model. If using Azure OpenAI, this should be the Azure
925
+ endpoint URL (eg "https://my-azure-openai-resource.openai.azure.com/") and the azure_deployment_name
926
+ parameter should also be set.
927
+ azure_deployment_name (Optional[str]): Used when an organization has its own deployment of an LLM model in
928
+ Azure, possibly for privacy reasons. Be sure to provide the llm_base_url parameter as well which should
929
+ be the Azure endpoint URL.
913
930
 
914
931
  Returns:
915
932
  Union[BaseModel, Dict, str]: The augmented metadata.
@@ -936,7 +953,8 @@ class MetadataEditor:
936
953
  output_mode="pydantic",
937
954
  metadata_producer_organization="My Organization",
938
955
  prefix="<AI>",
939
- azure_llm_base_url="https://my-azure-openai-resource.openai.azure.com/",
956
+ llm_base_url="https://my-azure-openai-resource.openai.azure.com/",
957
+ azure_deployment_name="my-llm-deployment"
940
958
  )
941
959
  ```
942
960
  """
@@ -1008,7 +1026,7 @@ class MetadataEditor:
1008
1026
  tokenizer_model=tokenizer_model,
1009
1027
  max_tokens=max_tokens,
1010
1028
  llm_base_url=llm_base_url,
1011
- azure_llm_base_url=azure_llm_base_url,
1029
+ azure_deployment_name=azure_deployment_name,
1012
1030
  )
1013
1031
 
1014
1032
  if new_metadata is None or len(new_metadata) == 0:
@@ -1043,7 +1061,7 @@ class MetadataEditor:
1043
1061
  document, geospatial, image, indicator, indicators_db, microdata, resource, script, table, video
1044
1062
  In this case we will use the default template for that metadata type.
1045
1063
  Alternatively you can pass in the UID of a template. This is required if the metadata is a dictionary
1046
- otherwise ignored.
1064
+ otherwise the UID associated with the pydantic model or the Excel file will be used.
1047
1065
 
1048
1066
  Returns:
1049
1067
  int: The ID of the newly created document metadata
@@ -1063,6 +1081,37 @@ class MetadataEditor:
1063
1081
  )
1064
1082
  return ret["id"]
1065
1083
 
1084
+ # def log_project_admin_metadata(
1085
+ # self, id: int, metadata: Union[BaseModel, Dict, str], metadata_type_or_template_uid: Optional[str] = None
1086
+ # ) -> int:
1087
+ # """Validates and logs admin metadata which can be a dictionary, a pydantic model or a path to an Excel file.
1088
+
1089
+ # Args:
1090
+ # id (int): The ID of the project to associate the metadata with.
1091
+ # metadata (dictionary or BaseModel or str): If str, it's assumed this is a path to an appropriately
1092
+ # formatted Excel file.
1093
+ # admin_metadata_template_uid (str, optional): The UID of the admin metadata template to use.
1094
+ # otherwise the UID associated with the pydantic model or the Excel file will be used.
1095
+
1096
+ # Returns:
1097
+ # int: The ID of the newly created admin metadata
1098
+ # """
1099
+ # metadata, metadata_type, uid = self._process_metadata_input(metadata, metadata_type_or_template_uid)
1100
+ # post_request_pth = "admin-metadata/data/"
1101
+ # post_json = {
1102
+ # "project_id": id,
1103
+ # "template_uid": uid,
1104
+ # "metadata": remove_empty_from_dict(metadata.model_dump(mode="json",
1105
+ # exclude_none=True,
1106
+ # exclude_unset=True)),
1107
+ # }
1108
+ # ret = self._apinterface.post_request(
1109
+ # pth=post_request_pth,
1110
+ # json=post_json,
1111
+ # id=id,
1112
+ # )
1113
+ # return ret
1114
+
1066
1115
  def update_project_log_by_id(self, id: int, new_metadata: Union[BaseModel, Dict, str]):
1067
1116
  """Updates the record of the metadata.
1068
1117
 
@@ -1252,7 +1301,6 @@ class MetadataEditor:
1252
1301
  new_index = templates.index.astype(int)
1253
1302
  except ValueError:
1254
1303
  pass
1255
- else:
1256
1304
  templates.index = new_index
1257
1305
  return templates
1258
1306
 
@@ -1272,10 +1320,23 @@ class MetadataEditor:
1272
1320
  raise KeyError(f"No result returned.\nResponse: {response}")
1273
1321
 
1274
1322
  temp = pd.Series(response["result"], name=response["result"]["name"])
1275
- self._templates[uid] = {"template": temp, "metadata_type": self._mm.standardize_metadata_name(temp.data_type)}
1276
1323
  try:
1277
- parent_schema = self._mm.metadata_class_from_name(temp.data_type)
1278
- klass = pydantic_from_template(temp.template, parent_schema=parent_schema, uid=uid, name=temp.name)
1324
+ standard_type_name = self._mm.standardize_metadata_name(temp.data_type)
1325
+ except ValueError:
1326
+ standard_type_name = temp.data_type
1327
+ self._templates[uid] = {"template": temp, "metadata_type": standard_type_name}
1328
+ try:
1329
+ try:
1330
+ parent_schema = self._mm.metadata_class_from_name(temp.data_type)
1331
+ except ValueError:
1332
+ warnings.warn(
1333
+ f"Could not find a metadata class for template type '{standard_type_name}', UID '{uid}'. "
1334
+ f"Building a class from the template without a parent schema."
1335
+ )
1336
+ parent_schema = SchemaBaseModel
1337
+ klass = pydantic_from_template(
1338
+ temp.template, parent_schema=parent_schema, uid=uid, name=temp.name, metadata_type=standard_type_name
1339
+ )
1279
1340
  self._templates[uid]["class"] = klass
1280
1341
  finally:
1281
1342
  return temp
@@ -1474,6 +1535,26 @@ class MetadataEditor:
1474
1535
  metadata["description"] = description
1475
1536
  self._apinterface.post_request("collections/update/{}", id=id, json=metadata)
1476
1537
 
1538
+ def copy_collection(self, source_id: int, target_id: int):
1539
+ """Copy projects and users from one collection to another.
1540
+
1541
+ Args:
1542
+ source_id (int): The ID of the source collection.
1543
+ target_id (int): The ID of the target collection.
1544
+
1545
+ """
1546
+ self._apinterface.post_request("collections/copy/", json={"source_id": source_id, "target_id": target_id})
1547
+
1548
+ def move_collection(self, source_id, target_id):
1549
+ """Move source collection to be a sub-collection of the target collection.
1550
+
1551
+ Args:
1552
+ source_id (int): The ID of the source collection.
1553
+ target_id (int): The ID of the target collection.
1554
+
1555
+ """
1556
+ self._apinterface.post_request("collections/move/", json={"source_id": source_id, "target_id": target_id})
1557
+
1477
1558
  def count_projects_in_collection(self, collection: int) -> int:
1478
1559
  """Count the number of projects you have access to.
1479
1560
 
@@ -617,7 +617,12 @@ def append_variables_and_data_files(model_elements, parent_schema, apply_rules:
617
617
 
618
618
 
619
619
  def pydantic_from_template(
620
- template: Dict, parent_schema: Type[SchemaBaseModel], uid: str, name: Optional[str] = None, apply_rules: bool = True
620
+ template: Dict,
621
+ parent_schema: Type[SchemaBaseModel],
622
+ uid: str,
623
+ name: Optional[str] = None,
624
+ metadata_type: Optional[str] = None,
625
+ apply_rules: bool = True,
621
626
  ) -> Type[BaseModel]:
622
627
  """Generate a Pydantic model from a given template.
623
628
 
@@ -626,6 +631,8 @@ def pydantic_from_template(
626
631
  parent_schema (Type[SchemaBaseModel]): The parent schema model to base the new model on.
627
632
  uid (str): A unique identifier for the new model.
628
633
  name (Optional[str], optional): The name of the new model. Defaults to None.
634
+ metadata_type (Optional[str], optional): The metadata type for the new model used if the parent_schema is
635
+ SchemaBaseModel. Otherwise the metadata_type of the parent_schema is used. Defaults to None.
629
636
  apply_rules (bool, optional): Whether to apply rules to the model elements. Defaults to True.
630
637
 
631
638
  Returns:
@@ -659,4 +666,12 @@ def pydantic_from_template(
659
666
  hasattr(parent_schema._metadata_type__, "default") and parent_schema._metadata_type__.default == "microdata"
660
667
  ):
661
668
  model_elements = append_variables_and_data_files(model_elements, parent_schema, apply_rules=apply_rules)
662
- return create_model_for_template(model_elements, parent_schema, name, uid=uid)
669
+ new_model = create_model_for_template(model_elements, parent_schema, name, uid=uid)
670
+ if parent_schema is SchemaBaseModel:
671
+ if metadata_type is None:
672
+ raise ValueError("metadata_type must be provided if parent_schema is SchemaBaseModel. ")
673
+ new_model._template_name__ = f"{name.replace('-', '_').replace(' ', '_')}"
674
+ new_model._template_uid__ = uid
675
+ new_model._metadata_type__ = metadata_type.replace("-", "_").replace(" ", "_")
676
+ new_model._metadata_type_version__ = "unknown"
677
+ return new_model
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pymetadataeditor"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Metadata Editor client for python"
5
5
  authors = ["Gordon Blackadder <gblackadder@worldbank.org>"]
6
6
  readme = "README.md"
@@ -12,10 +12,10 @@ requests = "^2.32.3"
12
12
  urllib3 = "^2.2.2" # earlier versions have known security issues
13
13
  pandas = "^2.2.2"
14
14
  pydantic = "^2.7.4"
15
- openai = "^1.59.4"
15
+ openai = "^2.8.0"
16
16
  tiktoken = "^0.8.0"
17
17
  markitdown = "^0.0.1a3"
18
- metadataschemas = "^0.1.2"
18
+ metadataschemas = "^0.1.3"
19
19
 
20
20
  [tool.poetry.group.dev.dependencies]
21
21
  pytest = "^8.2.2"