Package: unicat
Unicat client libraryUnicat is a Product Information Management SaaS.This library is still a work in progress, not all Unicat API options are covered yet.Documentation still needs a bit of work too.First, connect to Unicat (https://unicat.app):import sys
from unicat import Unicat
from .env import server, project_gid, api_key, local_folder

unicat = Unicat(server, project_gid, api_key, local_folder)
if not unicat.connect():
  print("Invalid connection settings")
  sys.exit(1)Download all assets for the project (you can find them in the local_folder):for asset in unicat.walk_asset_tree():
  if asset.is_file:
    asset.download()Or, write an XML product feed:with open("product-feed.xml", "w", encoding="utf-8") as f:
  f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  f.write('<products>\n')

  for record in unicat.walk_record_tree():
    if record.definition.name != "article":
      continue

    fields = record.fields["nl"]

    artnr = fields["artnr"].value
    price = fields["price"].value
    stock = fields["stock"].value

    f.write(f'  <product artnr="{artnr}" price="{price:%0.02f}" stock="{stock}"/>\n')
  f.write('</products>\n')There's also unicat.mutate, with options to update the Unicat project server-side, like unicat.mutate.create_record, unicat.mutate.modify_field and many more.TheunicatpackageThe Unicat package is split into reading/traversing and mutating. All mutating methods below are available on theunicat.mutateproperty. They will raise a UnicatError on error.Reading methodsunicat.get_record(gid:str,*,force:bool)->UnicatRecord|Noneunicat.get_records(gids:list[str],*,force:bool)->list[UnicatRecord]unicat.get_root_record()->UnicatRecordunicat.get_asset(gid:str,*,force:bool)->UnicatAsset|Noneunicat.get_asset_by_pathname(pathname:str,*,force:bool)->UnicatAsset|Noneunicat.get_assets(gids:list[str],*,force:bool)->list[UnicatAsset]unicat.get_assets_by_pathname(pathnames:list[str],*,force:bool)->dict[str,UnicatAsset]unicat.get_root_asset()->UnicatAssetunicat.get_definition(gid:str)->UnicatDefinition|Noneunicat.get_definitions(gids:list[str])->list[UnicatDefinition]unicat.get_definition_by_name(name:str)->UnicatDefinition|Noneunicat.get_definitions_by_name(names:list[str])->dict[str,UnicatDefinition]unicat.get_class(gid:str)->UnicatClass|Noneunicat.get_classes(gids:list[str])->list[UnicatClass]unicat.get_class_by_name(name:str)->UnicatClass|Noneunicat.get_classes_by_name(names:list[str])->dict[str,UnicatClass]unicat.get_field(gid:str)->UnicatField|Noneunicat.get_fields(gids:list[str])->list[UnicatField]unicat.get_field_by_name(name:str)->UnicatField|Noneunicat.get_fields_by_name(names:list[str])->dict[str,UnicatField]unicat.get_query(gid:str)->UnicatQuery|Noneunicat.get_queries(gids:list[str])->list[UnicatQuery]unicat.get_record_query_by_name(name:str)->UnicatQuery|Noneunicat.get_record_queries_by_name(names:list[str])->dict[str,UnicatQuery]unicat.get_asset_query_by_name(name:str)->UnicatQuery|Noneunicat.get_asset_queries_by_name(names:list[str])->dict[str,UnicatQuery]unicat.get_schema_query_by_name(name:str)->UnicatQuery|Noneunicat.get_schema_queries_by_name(names:list[str])->dict[str,UnicatQuery]Traversing methodsunicat.walk_record_children(parent_record:UnicatRecord,channel:gid,ordering:gid)->Iterator[UnicatRecord]unicat.walk_record_tree(channel:gid,ordering:gid)->Iterator[UnicatRecord]unicat.walk_record_query(language:str,query:UnicatQuery,*,limit:int)->Iterator[tuple[int,UnicatRecord]]unicat.walk_asset_children(parent_asset:UnicatAsset)->Iterator[UnicatAsset]unicat.walk_asset_tree()->Iterator[UnicatAsset]unicat.walk_asset_query(language:str,query:UnicatQuery,*,limit:int)->Iterator[tuple[int,UnicatAsset]]unicat.walk_definitions()->Iterator[UnicatDefinition]unicat.walk_classes()->Iterator[UnicatClass]unicat.walk_fields()->Iterator[UnicatField]unicat.walk_schema_query(language:str,query:UnicatQuery,*,limit:int)->Iterator[tuple[int,UnicatQuery]]unicat.walk_queries()->Iterator[UnicatQuery]Propertiesunicat.project:UnicatProjectUnicatProjectproperties & methodsproject.gid:gidproject.name:strproject.owner:UnicatUserproject.icon:str# used to construct /media urlproject.status:strproject.languages:list[str]project.default_language:strproject.channels:dict[str,gid]project.default_channel:gidproject.channel_name(key:gid)->strproject.orderings:dict[str,gid]project.default_ordering:gidproject.ordering_name(key:gid)->strproject.fieldlists:dict[str,gid]project.default_fieldlist:gidproject.fieldlist_name(key:gid)->strproject.backups:list[UnicatProjectBackup]# new in v0.6.0project.get_backup(version:int)->UnicatProjectBackup|None# new in v0.6.0project.members:list[UnicatProjectMember]UnicatUserproperties & methodsuser.gid:giduser.username:struser.name:struser.avatar:str# used to construct /media urlUnicatProjectMemberproperties & methodsprojectmember.project:UnicatProjectprojectmember.user:UnicatUserprojectmember.status:strprojectmember.roles:list[str]projectmember.options:dictprojectmember.key:strUnicatProjectBackupproperties & methodsNew in v0.6.0backup.version:intbackup.name:strbackup.created_by:strbackup.timestamp:timestamp# 1610635123.351925UnicatDefinitionproperties & methodsdefinition.gid:giddefinition.original:UnicatDefinition|Nonedefinition.name:strdefinition.label:dict[str,str]# key is languagedefinition.classes:list[UnicatClass]definition.classes_as_gids:list[gid]definition.fields:list[UnicatField]definition.fields_as_gids:list[gid]definition.titlefield:UnicatFielddefinition.fieldlists:dict[str,list[UnicatField]]# key is fieldlist keydefinition.layout:UnicatLayoutdefinition.childdefinitions:list[UnicatDefinition]definition.is_base:booldefinition.is_new:booldefinition.is_extended:booldefinition.is_working_copy:booldefinition.is_committed:booldefinition.all_fields:list[UnicatField]definition.base_classes:list[UnicatClass]definition.base_fields:list[UnicatField]definition.all_base_fields:list[UnicatField]definition.extended_classes:list[UnicatClass]definition.extended_fields:list[UnicatField]definition.all_extended_fields:list[UnicatField]UnicatClassproperties & methodsclass_.gid:gidclass_.original:UnicatClass|Noneclass_.name:strclass_.label:dict[str,str]# key is languageclass_.fields:list[UnicatField]class_.fields_as_gids:list[gid]class_.layout:UnicatLayoutclass_.is_new:boolclass_.is_working_copy:boolclass_.is_committed:boolUnicatFieldproperties & methodsfield.gid:gidfield.original:UnicatField|Nonefield.name:strfield.type:strfield.class_:UnicatClass|Nonefield.options:dictfield.is_localized:boolfield.is_required:boolfield.label:dict[str,str]# key is languagefield.initial:dict[str,str]# key is languagefield.unit:strfield.title:dict[str,str]# key is languagefield.is_new:boolfield.is_working_copy:boolfield.is_committed:boolUnicatLayoutproperties & methodslayout.gid:gidlayout.original:UnicatLayout|Nonelayout.name:strlayout.root:gidlayout.components:dict[gid,dict]layout.is_new:boollayout.is_working_copy:boolUnicatQueryproperties & methodsquery.gid:gidquery.type:str# schema, record, or assetquery.name:strquery.q:strquery.filter:listUnicatAssetproperties & methodsasset.gid:gidasset.pathname:strasset.path:strasset.name:strasset.is_file:boolasset.type:strasset.childcount:intasset.status:strasset.is_deleted:boolasset.info:dictasset.transforms:dict[str,dict]|Noneasset.default_transform:dict|Noneasset.title:dict[str,str]# key is languageasset.description:dict[str,str]# key is languageasset.updated_on:timestamp# 1610635123.351925asset.publish()->str# public_urlasset.publish_transformed(transform:UnicatTransform|None)->str# public_urlasset.download()->False|None|str# local_filepathasset.download_transformed(transform:UnicatTransform|None)->False|None|str# local_filepathUnicatRecordproperties & methodsrecord.gid:gidrecord.canonical:gidrecord.parent:gidrecord.backlinks:list[gid]record.is_link:boolrecord.is_deleted:boolrecord.treelevel:intrecord.path:list[gid]record.title:dict[str,str]# key is languagerecord.channels:list[gid]# enabled channels onlyrecord.orderings:dict[gid,int]record.childcount:intrecord.definition:UnicatDefinitionrecord.fields:dict[str,dict[str,UnicatRecordField]]# key is language, then fieldnamerecord.validation_report:None|list[dict[str,Any]]# validation result after updatesUnicatRecordFieldproperties & methodsrecordfield.field:UnicatFieldrecordfield.value:AnyA record field can have a value, a reference (record, asset), or it can be nested for class-fields.We also support 'list' versions of these.Note: this will probably be refactored in an upcoming versionthen, it may work like recordfield.label["en"] == "Image"and, for e.g. image fields, recordfield.value.pathname == "/asset-1-name.svg"# for valuesartnr=record.fields[language]["artnr"]# a recordfieldartnr.value# "CMS 225-945"artnr.field.label# "Article number"artnr.field.type# "textline"# for referencesimage=record.fields[language]["image"]# both recordfield and assetimage.value# "a0a80c9c-fa1b-4573-ac98-b7b07c81b583"image.field.label# "Image"image.pathname# "/products/cms225.eps"# for class fieldsdimensions_interior=record.fields[language]["dimensions_interior"]# a recordfield and classfielddimensions_interior.value# {"width__mm": 374, …}dimensions_interior["width__mm"].value# 374  -- this is a recordfield# for list valuescolors=record.fields[language]["colors"]colors.value# ["Red", "Blue"]colors.field.label# "Colors"colors[0].value# "Red" -- this is just a string# for list referencesimages=record.fields[language]["images"]images.value# ["a0a80c9c-fa1b-4573-ac98-b7b07c81b583", ]images.field.label# "Images"images[0]# this is just an assetimages[0].pathname# "/products/cms225.eps"# for classlist fieldstablespecs=record.fields[language]["tablespecs"]tablespecs.value# [{"width__mm": 7, …}, …]tablespecs.field.label# "Table specs"tablespecs[0]# this is recordfield-liketablespecs[0]["width__mm"].value# 7tablespecs[0]["width__mm"].field.label# "Width"Mutating project settingsmutate.add_language(language:str)->boolmutate.remove_language(language:str)->boolmutate.create_channel(name:str)->gid# gid type is actually a stringmutate.delete_channel(gid:gid)->boolmutate.create_ordering(name:str)->gidmutate.delete_ordering(gid:gid)->boolmutate.create_fieldlist(name:str)->gidmutate.delete_fieldlist(gid:gid)->boolMutating definitionsmutate.create_definition(*,name:str,label:dict[str,str],classes:list[UnicatClass],fields:list[UnicatField],titlefield:UnicatField,childdefinitions:list[UnicatDefinition])->UnicatDefinitionmutate.modify_definition(definition:UnicatDefinition,*,name:str,label:dict[str,str],classes:list[UnicatClass],fields:list[UnicatField],titlefield:UnicatField,childdefinitions:list[UnicatDefinition])->UnicatDefinitionmutate.modify_definition_modify_layout(definition:UnicatDefinition,*,name:str,root:gid,components:dict[gid,dict])->UnicatDefinitionmutate.modify_definition_add_class(definition:UnicatDefinition,class_:UnicatClass)->UnicatDefinitionmutate.modify_definition_remove_class(definition:UnicatDefinition,class_:UnicatClass)->UnicatDefinitionmutate.modify_definition_add_field(definition:UnicatDefinition,field:UnicatField)->UnicatDefinitionmutate.modify_definition_remove_field(definition:UnicatDefinition,field:UnicatField)->UnicatDefinitionmutate.modify_definition_fieldlist_add_field(definition:UnicatDefinition,fieldlist:gid,field:UnicatField)->UnicatDefinitionmutate.modify_definition_fieldlist_remove_field(definition:UnicatDefinition,fieldlist:gid,field:UnicatField)->UnicatDefinitionmutate.modify_definition_add_childdefinition(definition:UnicatDefinition,childdefinition:UnicatDefinition)->UnicatDefinitionmutate.modify_definition_remove_childdefinition(definition:UnicatDefinition,childdefinition:UnicatDefinition)->UnicatDefinitionmutate.commit_definition(new_or_working_copy:UnicatDefinition)->UnicatDefinitionmutate.save_as_new_definition(working_copy:UnicatDefinition)->UnicatDefinitionmutate.delete_definition(definition:UnicatDefinition)->boolMutating classesmutate.create_class(*,name:str,label:dict[str,str],fields:list[UnicatField])->UnicatClassmutate.modify_class(class_:UnicatClass,*,name:str,label:dict[str,str],fields:list[UnicatField])->UnicatClassmutate.modify_class_modify_layout(class_:UnicatClass,*,name:str,root:gid,components:dict[gid,dict])->UnicatClassmutate.modify_class_add_field(class_:UnicatClass,field:UnicatField)->UnicatClassmutate.modify_class_remove_field(class_:UnicatClass,field:UnicatField)->UnicatClassmutate.commit_class(new_or_working_copy:UnicatClass)->UnicatClassmutate.save_as_new_class(working_copy:UnicatClass)->UnicatClassmutate.delete_class(class_:UnicatClass)->boolMutating fieldsmutate.create_field(*,name:str,type:str,is_localized:bool,is_required:bool,label:dict,unit:str,initial:dict,options:dict)->UnicatFieldmutate.modify_field(field:UnicatField,*,name:str,type:str,is_localized:bool,is_required:bool,label:dict,unit:str,initial:dict,options:dict)->UnicatFieldmutate.commit_field(new_or_working_copy:UnicatField)->UnicatFieldmutate.save_as_new_field(working_copy:UnicatField)->UnicatFieldmutate.delete_field(field:UnicatField)->boolMutating recordsmutate.create_record(parent:UnicatRecord,ordering:gid)->UnicatRecordmutate.set_record_definition(record:UnicatRecord,definition:UnicatDefinition)->UnicatRecordmutate.extend_record_definition_add_class(record:UnicatRecord,class_:UnicatClass)->UnicatRecordmutate.extend_record_definition_add_field(record:UnicatRecord,field:UnicatField)->UnicatRecordmutate.extend_record_definition_add_fieldlist_field(record:UnicatRecord,fieldlist:gid,field:UnicatField)->UnicatRecordmutate.update_record(record:UnicatRecord,localizedfielddata:dict)->UnicatRecordmutate.set_record_channels(record:UnicatRecord,channels:list[gid],enabled:bool)->UnicatRecordmutate.copy_record_channels_from_parent(record:UnicatRecord,channels:list[gid]|None)->UnicatRecordmutate.copy_record_channels_down(record:UnicatRecord,channels:list[gid]|None,return_job:Bool=False)->UnicatRecord|UnicatJobmutate.copy_record_channels_up(record:UnicatRecord,channels:list[gid]|None)->UnicatRecordmutate.set_record_orderings(record:UnicatRecord,orderings:dict)->UnicatRecordmutate.link_record(parent:UnicatRecord,record:UnicatRecord,ordering:gid)->UnicatRecordmutate.delete_record(record:UnicatRecord)->UnicatRecordmutate.undelete_record(record:UnicatRecord)->UnicatRecordmutate.permanent_delete_record(record:UnicatRecord,return_job:Bool=False)->UnicatRecord|UnicatJobMutating assetsmutate.upload_asset(localfilepath:Path|str,folderasset:UnicatAsset)->UnicatAssetmutate.upload_update_asset(localfilepath:Path|str,asset:UnicatAsset)->UnicatAssetmutate.create_asset(parentasset:UnicatAsset,name:str)->UnicatAssetmutate.update_asset(asset:UnicatAsset,name:str,title:dict,description:dict)->UnicatAssetmutate.delete_asset(asset:UnicatAsset)->UnicatAssetmutate.undelete_asset(asset:UnicatAsset)->UnicatAssetmutate.permanent_delete_asset(asset:UnicatAsset)->boolMutating queriesmutate.create_query(type:str,name:str,q:str,filter:list)->UnicatQuerymutate.update_query(query:UnicatQuery,name:str,q:str,filter:list)->UnicatQuerymutate.delete_query(query:UnicatQuery)->boolBackupsNew in v0.6.0mutate.create_backup(created_by:str,name:str,return_job:Bool=False)->UnicatProjectBackup|UnicatJobmutate.update_backup(backup:UnicatProjectBackup,name:str)->UnicatProjectBackupmutate.restore_backup(backup:UnicatProjectBackup,return_job:Bool=False)->UnicatProject|UnicatJobmutate.delete_backup(backup:UnicatProjectBackup)->UnicatProjectmutate.delete_backups(backups:list[UnicatProjectBackup])->UnicatProjectJobsSome mutating methods can return a job if requested, so you can choose to wait for completion yourself (trackmethod), or ignore it and let it finish in the background some time. The job always has thereturn_valuefrom the (mutating) method available -- this is the "immediately returned" value, not some result from running the actual job (look instatusandinfoinstead).Usage:# by default, the method waits for completion before returningrecord=unicat.mutate.copy_record_channels_down(record,channels)# but you can also track progress yourselfjob=unicat.mutate.copy_record_channels_down(record,channels,return_job=True)forstatusinjob.track():assertstatus==job.statusprint(job.name,job.status)record=job.return_value# or, return quickly and let the job run unmonitored in the backgroundjob=unicat.mutate.copy_record_channels_down(record,channels,return_job=True)record=job.return_valueUnicatJobproperties & methodsjob.gid:gidjob.name:strjob.status:strjob.info:dictjob.created_on:timestamp|Nonejob.updated_on:timestamp|Nonejob.progress:dict# combined gid, name, status, info, and timestampsjob.return_value:Anyjob.track(timeout_in_seconds:float|None=None,poll_interval_in_seconds:float=1.0)->Generator[str]Error handlingWe handle errors with theUnicatErrorexception.fromunicatimportUnicat,UnicatErrorfromconfigimportPROJECT_GID,SECRET_API_KEY,LOCAL_ASSET_FOLDERunicat=Unicat("https://unicat.app",PROJECT_GID,SECRET_API_KEY,LOCAL_ASSET_FOLDER)ifnotunicat.connect():raiseException("Invalid connection settings")...try:unicat.mutate.update_record(record,{language:fields_data})exceptUnicatErrorase:print(e,e.code,e.message,e.info)The.code,.message, and.infoproperties match the API error result.Asset transform helperfromunicatimportUnicatTransformWe use this on assets, for publishing and/or downloading transformed versions.transform=UnicatTransform(resize="fill",width=400,height=300,type="jpg",dpr=2,optimize=True)public_url=asset.publish_transformed(transform)transform.merge(UnicatTransform(width=200,height=200,key="thumb"))# keeps type, dpr, etclocal_pathname=asset.download_transformed(transform)AUnicatTransfromaccepts any combination of the following arguments.name
key
force
optimize
resize
width
height
type
hotspot
crop
padding
quality
background
dprEach argument explained:name = "seo-optimized-name"Default: use source filenameUse this as the filename instead of the source filename. Mustn't include the extension.key = "2x"Default: auto-generate a key from a hash of the optionsIf you make multiple transforms from the same file, you can use keys to individualize them. They are included in the filename after the name and before the extension. A key is prepended by a '@', so we would get /filename@2x.jpg. You can use @'s in filenames and in keys, just make sure that the combinations add up to a unique final filename.type = "jpg"Options: jpg, png, or gifBoth extension and transformed file type. If you don't specify this, the source extension is used, which can lead to faulty results if it isn't one of the supported file types (jpg, png, or gif).force = TrueDefault: FalseIf force isn't enabled, no transformation is done if a file with the transform filename exists and is newer than the source.optimize = TrueDefault: FalseWe support pngcrush, jhead, jpegoptim, jpeg-recompress, gifsicle, scour, and svgo to strip down and compress the transformed file. Since this is a time-consuming process, it is disabled by default.resize = "fill"Options: width, height, fit, or fillResizing will always respect the aspect ratio. Placement of the resized source on the canvas is controlled by the width, height, hotspot, crop, and padding options. Images are never scaled up, only down.width = 400Resulting width of the transformed asset, in logical pixels (see alsodpr).Value is capped at 5000 pixels.height = 300Resulting height of the transformed asset, in logical pixels (see alsodpr).Value is capped at 5000 pixels.hotspot = (0.5, 0.5)Default: 0.5,0.5 (the center)The hotspot serves two purposes: first to place the resized image on the canvas with the hotspot as close as possible to the center of the canvas, and second as the centerpoint for the crop transform if one is requested.
The hotspot is given as an x,y coordinate, with values as a fraction of the width and height, respectively. Valid values are 0.0 through 1.0 for each.crop = (0.6, 0.6)Default: don't cropUse crop to select an area from the source that will then be resized to be placed on the canvas. The crop is centered on the hotspot.
The crop is given as w,h dimensions, with values as a fraction of the width and height, respectively. Valid values are 0.0 through 1.0 for each.padding = (0, "auto", 0, "auto")Default:auto,auto,auto,autoSpecify padding in the target image. The values are for top, right, bottom, and left padding (clockwise starting at top). If a value is set toauto, that padding will grow to fill available space. If two opposing sides have non-autovalues, they will get at least the specified padding, plus half of the remaining available space each. If you want to anchor the image to the top-right of the canvas, you can specify 0,0,"auto","auto".Values are capped at 1000 pixels.background = "abcdef"Default: transparent (or white if transformed file doesn't support transparency)Specify a background color to use for transparent areas in the source and for any padding added.
There are two predefined options,transparentandwhite. Any other background color must be specified as an rgb hex value, similar to CSS but without the # sign. Use the full 6-character rgb hex, not a CSS shortcut like bbb.quality = 82Default: 100Lower quality leads to smaller filesizes, but higher quality looks better. This is a compromise, use your best judgment.
Allowed values are 0 through 100.dpr = 2Default: 1Device pixel ratio is abbreviated dpr, and is used to indicate the number of physical pixels the device screen has per logical pixel. You always specify the resizing and padding values in logical pixels. Regular screens have a dpr of 1. High resolution or retina screens have a dpr of 2 or 3. We support a max dpr of 4, just in case. A dpr of 2 means that for a requested width of 400, you get a transformed image with 800 (physical) pixels.unicat.utilsfromunicat.utilsimport*defgid()->str# a uuid4defmaybe_gid(possible_gid:Any)->booldeftest_true(data:Any)->booldeftest_false(data:Any)->booldefmake_bool(any:Any)->booldefmake_bool_str(any:Any)->strdefmake_str(any:Any)->strdefmake_json_str(any:Any)->strdefmake_json(any:Any)->Anydefmake_json_list(any:Any)->list[Any]defmake_int(any:Any)->intdefmake_float(any:Any)->floatdefnoop(any:Any)->Anydefmake_str_list(any:Any)->list[str]defconvert_value_to_fielddata(fieldtype:str,value:Any)->bool|str|Any|int|float|list[str]|list[Any]"""Return a value suitable for writing to Unicat, for list field types thismeans converting newline-separated entries to lists.For class or classlist fields, we produce a JSON data structure.This can raise exceptions when the value doesn't match the type, e.g. converting astring to a float."""defconvert_fielddata_to_value(fieldtype:str,fielddata:Any)->str|int|float"""Return a value suitable for writing in a cell, such as a text or a number.Lists will be flattened by stringifying each item and concatenating with \n.For class or classlist fields, we will have pretty-printed JSON."""defmerge_dicts(a:dict,b:dict)->dict"""Merge b into a, returning a.a is updated; if you don't want to mutate a, call it as `merge_dicts(dict(a), b)`."""defdiff_record_fields_data(unicat:Unicat,record:UnicatRecord,localizedfielddata:dict)->dict# dict format: d[language][fieldname] = value"""Return a version of localizedfielddata that only has data that is different fromthe raw record data."""defhash_text(text:str)->str# unicode okdefhash_data(data:Any)->str# data must be json-serializableclassDuckObject# quickly construct an object-like duck from a dict or kwargs, see belowclassFieldColumns# flatten a list of (class-)fields, see belowDuckObjectDuckObjects are used to quickly construct an object-like duck.Uses keyword arguments to construct any duck-like object with those attributes.ducklike=DuckObject(walk="waddle",talk="quack")assertducklike.walk=="waddle"assertducklike.talk=="quack"assertnothasattr(ducklike,"bill")duckquery=DuckObject(q="",filter=["value","is",[base_artnr_field.gid,article.base_artnr]])print(duckquery.q,duckquery.filter)duckrecord=DuckObject(gid="<anything gid-like>")print(duckrecord.gid)FieldColumnsFieldColumns are needed for class fields, that have nested subfields.With FieldColumns, we can flatten a list of fields to a list of columns,
associated with those fields.Each column has a fieldstack, that is a list of the field and nested subfields.
For a regular field, the fieldstack is just that field.
A classfield with three subfields will yield three columns, and each column has
a fieldstack with the classfield first, and then the subfield for that column.
If that subfield is another classfield, we'll get more columns and deeper
stackfields.For example, you have[image, dimensions]as fields, anddimensionsis of type class, and has subfieldswidthandlength; FieldColumns will give you[image, dimensions.width, dimensions.length], useable for writing to tab-separated files or spreadsheets.Most of the time, you won't need the stackfield in the client code, see also the
_FieldColumn implementation where we get the 'name' for a column by walking the
fieldstack.Client code can look something like this, for reading from Unicat:columns=FieldColumns(fields,prefix_column_count=3)forcolumnincolumns:column_name=column.name()# e.g. 'image' or 'dimensions.width__mm'column_label=column.label()# e.g. 'Image' or 'Dimensions\nWidth [mm]'column_value=Noneerror=Nonetry:record_field=column.extract_record_field(record.fields[language])column_value=record_field.valueifrecord_fieldelseNoneexceptKeyError:error=("Field is not part of this definition. Do not enter data here.")FieldColumns can also be used to write to Unicat, when we need a record's fields
data structure:recordfields_data={}forcolumnincolumns:fieldvalue=row[column.index].valuecolumn.update_fields_data(recordfields_data,fieldvalue)unicat.mutate.update_record(record,{language:recordfields_data})Changelogv0.7.0 - s_by_(path)name fix and cleanupRenamedget_assets_by_pathnamestoget_assets_by_pathname, and changed the return
value to a dict keyed by pathname, for consistency with other by_name functions.unicat.get_assets_by_pathname(pathnames:list[str],*,force:bool)->dict[str,UnicatAsset]Fixed docs for by_name functions that fetch multiple items - they return dicts keyed by
name. Fixed returned values for those to return empty dicts when the names list is
empty.unicat.get_definitions_by_name(names:list[str])->dict[str,UnicatDefinition]unicat.get_classes_by_name(names:list[str])->dict[str,UnicatClass]unicat.get_fields_by_name(names:list[str])->dict[str,UnicatField]unicat.get_record_queries_by_name(names:list[str])->dict[str,UnicatQuery]unicat.get_asset_queries_by_name(names:list[str])->dict[str,UnicatQuery]unicat.get_schema_queries_by_name(names:list[str])->dict[str,UnicatQuery]Fixedwalk_schema_querycrash.Fixed error-reporting from a UnicatJob gone wrong.v0.6.0 - backupsNew project properties and methodsproject.backups:list[UnicatProjectBackup]project.get_backup(version:int)->UnicatProjectBackup|NoneNew UnicatProjectBackup classbackup.version:intbackup.name:strbackup.created_by:strbackup.timestamp:timestamp# 1610635123.351925New mutation methodsmutate.create_backup(created_by:str,name:str,return_job:Bool=False)->UnicatProjectBackup|UnicatJobmutate.update_backup(backup:UnicatProjectBackup,name:str)->UnicatProjectBackupmutate.restore_backup(backup:UnicatProjectBackup,return_job:Bool=False)->UnicatProject|UnicatJobmutate.delete_backup(backup:UnicatProjectBackup)->UnicatProjectmutate.delete_backups(backups:list[UnicatProjectBackup])->UnicatProjectv0.5.6 - all the things that came before we had a changelogAll the things that came before