Pages

Showing posts with label xml. Show all posts
Showing posts with label xml. Show all posts

Monday, October 16, 2017

Python for Salesforce Developers: "Just Push Play" Run-Specified-Tests Code Deployment

Warning: the attached Python script makes you put a password straight into the code and helps you make an end-run around all sorts of change-management best practices.

Basically, if you're not a little horrified to see this script publicly shared, you probably don't understand what it's capable of enough to be using it -- so please dont!

That said, for circumstances in which you were going to ignore a lot of change-management issues anyway, or just want to do a "check only" deploy of code to a Salesforce org, etc., this code basically lets you type a handful of classes / pages / etc. into a Python script, type in your username & password, say which tests you want to run and how you want to deploy it, and see the results really quickly.

The idea is to be almost as handy, to a developer, for small changes, as right-clicking on code files in Eclipse and "deploying" from there -- the problem with Eclipse being that it doesn't have a "Run Specified Tests" option.

At some point I might create something similar to this that lets you log into two Salesforce orgs, download a smattering of code from one to your local hard drive (instead of already having to have downloaded it with Eclipse), and proceed with the deploy from there. That really feels like playing with laziness/sloppiness fire, though (somehow seems to take out the "Would I really have deployed this just with Eclipse, anyway?" factor).

This code is probably best just for "checkOnly=TRUE" deploys. For real deploys, it's probably still best to Run All Tests, and using a "Change Set" is a lot better for anyone else who might have to stumble into your org a few weeks later and see what's been happening as far as code deploys. (And that's just a bare minimum of version control for simply-maintained orgs.)

A few notes on the code:

  • Change "#'''" to "'''" around blocks of code to quickly toggle them off (no point, for example, re-logging in if your Python IDE already has your session ID in memory, and you don't want to fire up a new "deploy" just to run the "check deployment status" code again).
  • "thingsToAdd" is where most of the "what you need to type" exists.
  • You'll also need to set "username" & "password" values (including your security token appended to your password) in "toOrgLoginEnvelope" -- I recommend remembering to change it back by saving this script to your hard drive (if you do save it) with a filename that includes something like "INCLUDES PASSWORD" so, as you exit the IDE, you remember to change things back.
  • "inputstr" will need the actual path to where you've used Eclipse to download the files you have "ready to deploy" to on your hard drive.
  • "outpbase" should be somewhere easy to find and delete later, like a sub-folder on your desktop.
  • "deployEnvelope" will need "checkOnly" set inside the XML itself, "testLevel" set just above it, and a (brackets-and-comma-delimited) list of tests to run towards the end of the "runtests" parameter inside the parentheses that follow the XML (if n/a, use "[]").
import os
import shutil
import base64
from xml.etree import ElementTree
import xml.dom.minidom
import requests
import re   

def getUniqueElementValueFromXmlString(xmlString, elementName):
    #Extracts an element value from an XML string.
    #For example, invoking getUniqueElementValueFromXmlString('<?xml version="1.0" encoding="UTF-8"?><foo>bar</foo>', 'foo') should return the value 'bar'.
    elementsByName = xml.dom.minidom.parseString(xmlString).getElementsByTagName(elementName)
    elementValue = None
    if len(elementsByName) > 0: elementValue = elementsByName[0].toxml().replace('<' + elementName + '>', '').replace('</' + elementName + '>', '')
    return elementValue

metadataAPIVer = '40.0'

#'''
toOrgLoginEnvelope = """<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Body><login xmlns="urn:partner.soap.sforce.com"><username>{username}</username><password>{password}</password></login></soapenv:Body></soapenv:Envelope>""".format(
username='username', password='password')
tor = requests.post('https://login.salesforce.com/services/Soap/u/'+metadataAPIVer, toOrgLoginEnvelope, headers={'content-type':'text/xml','charset':'UTF-8','SOAPAction':'login'})
tosessid = getUniqueElementValueFromXmlString(tor.content, 'sessionId')
tohost = getUniqueElementValueFromXmlString(tor.content, 'serverUrl').replace('http://', '').replace('https://', '').split('/')[0].replace('-api', '')
toorgid = re.sub(r'^.*/([a-zA-Z0-9]{15})$', r'\1', getUniqueElementValueFromXmlString(tor.content, 'serverUrl'))
toapiver = re.sub(r'^.*/([0-9.]+)/[a-zA-Z0-9]{15}$', r'\1', getUniqueElementValueFromXmlString(tor.content, 'serverUrl'))
#'''

# EXAMPLE CODE:  thingsToAdd = {'classes':[''],'pages':['']}
thingsToAdd = {'classes': {'singCaps':'ApexClass','ext':'cls','toUpl':['OpportunityETLHandler','OpportunityETLTest']}, 'pages': {'singCaps':'ApexPage','ext':'page','toUpl':['OpportunityETLPage']}}
inputstr = 'C:\\EXAMPLEFOLDER\\EclipseWorkspace\\My Sandbox\\src\\'
outpbase = 'C:\\EXAMPLETEMPFOLDER\\temppkgtouploadfromeclipse\\'
outpfiles = outpbase + '\\filesbeforezip\\'
outpzip = outpbase + 'uploadme'

#'''
# BEGIN:  Code to create package
if not os.path.exists(outpbase): os.makedirs(outpbase)
if not os.path.exists(outpfiles): os.makedirs(outpfiles)
pkgroot = ElementTree.Element('Package', attrib={'xmlns':'http://soap.sforce.com/2006/04/metadata'})
for folder in thingsToAdd.keys():
    innerDict = thingsToAdd[folder]
    typesElem = ElementTree.Element('types')
    if not os.path.exists(outpfiles+folder+'\\'): os.makedirs(outpfiles+folder+'\\')
    for item in innerDict['toUpl']:
        membersElem = ElementTree.Element('members')
        membersElem.text = item
        typesElem.append(membersElem)
        shutil.copy(inputstr+folder+'\\'+item+'.'+innerDict['ext'], outpfiles+folder+'\\'+item+'.'+innerDict['ext'])
        shutil.copy(inputstr+folder+'\\'+item+'.'+innerDict['ext']+'-meta.xml', outpfiles+folder+'\\'+item+'.'+innerDict['ext']+'-meta.xml')
    namesElem = ElementTree.Element('name')
    namesElem.text = innerDict['singCaps']
    typesElem.append(namesElem)
    pkgroot.append(typesElem)
verElem = ElementTree.Element('version')
verElem.text = metadataAPIVer
pkgroot.append(verElem)
with open(outpfiles+'package.xml', 'w', newline='') as fw:
    dom_string = xml.dom.minidom.parseString(ElementTree.tostring(pkgroot)).toprettyxml(encoding='UTF-8', indent='    ')
    dom_string = '\n'.join([s for s in dom_string.decode('UTF-8').splitlines() if s.strip()]) + '\n'
    fw.write(dom_string)
# END:  Code to create package
#'''

#'''
# BEGIN:  Code to create ZIP
shutil.make_archive(base_name=outpzip, format='zip', root_dir=outpfiles, base_dir='./')
zipString = None
with open(outpzip+'.zip', 'rb') as f: zipString = base64.b64encode(f.read()).decode('UTF-8')
# END:  Code to create ZIP
#'''

# TO DO:  Figure out how to run several tests.  Maybe it's just a comma-separation?  I think it it's that in the point-and-click Change Set UI.

#'''
# BEGIN:  Code to deploy ZIP
testLevel = 'RunSpecifiedTests' # Most common values will be 'RunSpecifiedTests' or 'RunLocalTests'
deployEnvelope = """<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:met="http://soap.sforce.com/2006/04/metadata">
      <soapenv:Header>
         <met:SessionHeader>
            <met:sessionId>{sessionid}</met:sessionId>
         </met:SessionHeader>
      </soapenv:Header>
      <soapenv:Body>
         <met:deploy>
             <met:zipFile>{zipfile}</met:zipFile>
             <met:deployOptions>
                 <met:checkOnly>true</met:checkOnly>
                 <met:rollbackOnError>true</met:rollbackOnError>
                 {runtests}
                 <met:singlePackage>true</met:singlePackage>
                 <met:testLevel>{testlev}</met:testLevel>
             </met:deployOptions>
         </met:deploy>
      </soapenv:Body>
      </soapenv:Envelope>""".format(sessionid=tosessid, zipfile=zipString, testlev=testLevel, runtests=''.join(['<met:runTests>'+x+'</met:runTests>' for x in ['OpportunityETLTest1','OpportunityETLTest2']]) if testLevel=='RunSpecifiedTests' else '')
deploytor = requests.post('https://'+tohost+'/services/Soap/m/'+toapiver+'/'+toorgid, deployEnvelope, headers={'content-type': 'text/xml', 'charset': 'UTF-8', 'SOAPAction': 'deploy'})
# END:  Code to deploy ZIP
#'''

#'''
# BEGIN:  Code to check deploy
checkDeployStatusEnvelope = """<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:met="http://soap.sforce.com/2006/04/metadata">
      <soapenv:Header>
         <met:SessionHeader>
            <met:sessionId>{sessionid}</met:sessionId>
         </met:SessionHeader>
      </soapenv:Header>
      <soapenv:Body>
         <met:checkDeployStatus>
             <met:asyncProcessId>{process_id}</met:asyncProcessId>
             <met:includeDetails>true</met:includeDetails>
         </met:checkDeployStatus>
      </soapenv:Body>
      </soapenv:Envelope>""".format(sessionid=tosessid, process_id=getUniqueElementValueFromXmlString(deploytor.content, 'id'))

checkdeploytor = requests.post('https://'+tohost+'/services/Soap/m/'+toapiver+'/'+toorgid, checkDeployStatusEnvelope, headers={'content-type': 'text/xml', 'charset': 'UTF-8', 'SOAPAction': 'checkDeployStatus'})

#print(checkdeploytor.content)
#print()
print('Id: ', getUniqueElementValueFromXmlString(checkdeploytor.content, 'id'))
print('Done: ', getUniqueElementValueFromXmlString(checkdeploytor.content, 'done'))
print('Success: ', getUniqueElementValueFromXmlString(checkdeploytor.content, 'success'))
print('Status: ', getUniqueElementValueFromXmlString(checkdeploytor.content, 'status'))
print('Problem: ', getUniqueElementValueFromXmlString(checkdeploytor.content, 'problem'))
print('NumberComponentsTotal: ', getUniqueElementValueFromXmlString(checkdeploytor.content, 'numberComponentsTotal'))
print('RunTestResult: ', getUniqueElementValueFromXmlString(checkdeploytor.content, 'runTestResult'))
print('NumberTestsTotal: ', getUniqueElementValueFromXmlString(checkdeploytor.content, 'numberTestsTotal'))
#print(getUniqueElementValueFromXmlString(checkdeploytor.content, 'details'))
# END:  Code to check deploy
#'''

Tuesday, September 26, 2017

Python To Facilitate Adding A New Field To Multiple Custom Report Types (involves XML)

Quick code dump: if you download all your "report types" with Eclipse, this can help you put together "fresh" copies of them for use with the Workbench Deploy web site (just zip them up). Very handy if you just created a custom field on an object that's involved in dozens of report types.

This is an example of why it's good to be able to work with XML, too, in Python!

Hopefully I can clean up (or delete & replace) this post at a later date.

import os
from xml.etree import ElementTree
import xml.dom.minidom

def stripNSFromET(etRoot):
    etRoot.tag = etRoot.tag.split('}', 1)[1] # strip all namespaces level 1
    for el in etRoot:
        if '}' in el.tag:
            el.tag = el.tag.split('}', 1)[1] # strip all namespaces level 2
            for el3 in el:
                if '}' in el3.tag:
                    el3.tag = el3.tag.split('}', 1)[1] # strip all namespaces level 3
                    for el4 in el3:
                        if '}' in el4.tag:
                            el4.tag = el4.tag.split('}', 1)[1] # strip all namespaces level 4
                            for el5 in el4:
                                if '}' in el5.tag:
                                    el5.tag = el5.tag.split('}', 1)[1] # strip all namespaces level 5
    return etRoot

def isTableOfInterest(tableTagText, fieldsDatabaseTableAPIName):
    if fieldsDatabaseTableAPIName == 'Contact':
        return tableTagText == 'Contact' or tableTagText.endswith('.Contacts') or tableTagText.endswith('OpportunityContactRoles')
    else:
        return tableTagText == fieldsDatabaseTableAPIName or tableTagText.endswith('.'+fieldsDatabaseTableAPIName)

outpstr = 'C:\\example\\temppackagereporttypes\\'
pstr = 'C:\\examplepath\\EclipseWorkspace\\orgfolder\\src\\reportTypes\\'
fstr = 'C:\\examplepath\\EclipseWorkspace\\orgfolder\\src\\reportTypes\\Contact_With_Campaign_Members.reportType'

metadataAPIVer = '40.0'
fieldsDBTableAPIName = 'Contact'
fieldAPINames = ['New_Field_1__c','New_Field_2__c']
fieldAPINames.sort()

# SOME NOTES OF INTEREST:
# Contact is not only referred to in the normal way, but also often as OpportunityContactRole coming off an "Opportunity."  Not sure if ALL such OCRs are actually able to take Contact fields.
# This could make it a bit difficult to decide into what "Section," exactly, to add a new database field when adding it to every ReportType:
    # A given "Section" tag within a ReportType's XML file is NOT limited to containing "Columns" elements with the same database "Table" value.  For example, Graduate_Admissions.reportType, with a base object of Contact, has a section named "Recruit data" with fields from both the "Contact.Program_Interests__r" and "Contact" tables in it.
    # A given database "Table" value can appear in multiple "Section" tags within a ReportType's XML file (e.g. "Contact" fields could be spread across multiple sections).
    # CampaignMember often ends up repeated -- once as a top-level, once below itself -- in a ReportType where it is included (with fields duplicated and everything).  Good thing I'm not yet adding any CampaignMember fields to reports.

# SOME CODE TO REMEMBER:
# The number of tables mentioned in a ReportType file:  len({e.text for e in root.findall('./sections/columns/table')})
# The actual tables mentioned in a ReportType file:  {e.text for e in root.findall('./sections/columns/table')}
# The number of "sections" in a ReportType file:  len(root.findall('./sections'))
# The labels of the actual "sections" in a ReportType file:  {e.find('masterLabel').text for e in root.findall('./sections')}
# Iterating over a dict:  for k, v in d.items():


"""
Algorithm per ReportType file, parsed into an XML-ElementTree node called "root":
- Figure out how many 1st-level nodes called "sections" there are.
- For each 1st-level "section" node, if it's "of interest," set it aside as a key to a dict called "d",
    and a list of all its relevant child 2nd-level "columns" nodes as the values.
- Loop through "d" and set aside any keys that have the most list-items as a value, of all keys in "d";
    set aside a list of any such keys as "topSections"
- If there was just 1 such "top section" key, set that 1st-level "section" node aside as the variable "se"
    (This is, visually to the end user, where we'll be adding the new field.)
- If there was more than 1, arbitrarily pick 1 and set that 1st-level "section" node aside as the variable "se"
- Once we've picked a 1st-level "section" node as "se," cache a dict "uniqueTableNames" 
    of the distinct words appearing among its grandchild (3rd-level) "table" tag values as "key"
    and the count-of-table-tag-per-distinct-word as "value."
- Presuming there were any (I guess this serves as a sort of dummy-check for the section),
    instantiate a new ElementTree "columns" node called "newColumn,"
    append "newColumn" to "se" (make it 2nd-level)
    and flesh out "newColumn" with details of the field we want to add.
    (When fleshing it out, we arbitrarily pick a value for the 3rd-level "table" tag if "uniqueTableNames" had several keys.)

"""

changedThese = []
pkgroot = None
for i in os.listdir(pstr):
    root = None
    if not i.startswith('wrt_'):
        with open(pstr+i, 'r', encoding='utf-8') as f: # Open a ReportType metadata XML file
            root = stripNSFromET(ElementTree.parse(f).getroot()) # Store a reference to the root of the XML file currently open in a variable
            d = {}
            allFieldsForTableOfInterestColsInAllSectionsOfInterest = []
            for sec in root.findall('./sections'):
                columnsOfInterest = [e for e in sec.findall('columns/table/..') if isTableOfInterest(e.find('table').text, fieldsDBTableAPIName)]
                if len(columnsOfInterest) > 0:
                    d[sec] = columnsOfInterest # Add to "d" any "section" and a list of applicable "columns" inside it
            if len(d) < 1:
                continue # This file is not of interest if nothing got added to "d" (if it's not a ReportType that includes any fields of the object of interest) -- move on to next file
            else:
                print('\r\n' + i + ', baseObject:  ' + root.find('./baseObject').text) # Display which file we are working with and what its "Base Object" is
                se = None
                if len(d) > 0: # Why did I have this at ">1" when I found it 6 months later?  Should it be >0?
                    allFieldsForTableOfInterestColsInAllSectionsOfInterest = [item.find('field').text for sublist in d.values() for item in sublist]
                    if all(fldNm in allFieldsForTableOfInterestColsInAllSectionsOfInterest for fldNm in fieldAPINames):
                        continue # This file is not of interest if all fields are already in it
                    topSections = {k for k, v in d.items() if len(v) == max([len(arr) for arr in d.values()])}
                    if len(topSections) >= 1:
                        if len(topSections) == 1:
                            se = (next(iter(topSections))) # There was only 1 top-ranked section -- pick it
                        else:
                            topSecsWithLabelLikeTableNewFieldIsFrom = [s for s in topSections if s.find('masterLabel').text in [fieldsDBTableAPIName, fieldsDBTableAPIName+'s']]
                            if len(topSecsWithLabelLikeTableNewFieldIsFrom) > 0:
                                se = (next(iter(topSecsWithLabelLikeTableNewFieldIsFrom))) # I don't really care which it is, honestly
                            else:
                                se = (next(iter(topSections))) # I tried my best -- moving on.  Just picking a section.
                    else:
                        se = (next(iter(d.keys()))) # It was a 1-section file -- just pick the one section
                if se:
                    uniqueTableNames = {tbstr : [e.text for e in se.findall('columns/table')].count(tbstr) for tbstr in [e.text for e in se.findall('columns/table')]}
                    if len(uniqueTableNames) >= 1: # We can just tack our new column onto the only section that already has other Contact values
                        changedAnything = False
                        for fieldAPIName in fieldAPINames:
                            if fieldAPIName not in allFieldsForTableOfInterestColsInAllSectionsOfInterest:
                                changedAnything = True
                                newColumn = ElementTree.Element('columns')
                                se.append(newColumn)
                                newColumn.append(ElementTree.Element('checkedByDefault'))
                                newColumn.find('checkedByDefault').text = 'false'
                                newColumn.append(ElementTree.Element('field'))
                                newColumn.find('field').text = fieldAPIName
                                newColumn.append(ElementTree.Element('table'))
                                if len(uniqueTableNames) == 1:
                                    newColumn.find('table').text = next(iter(uniqueTableNames))
                                elif len(uniqueTableNames) > 1:
                                    newColumn.find('table').text = max(uniqueTableNames, key=uniqueTableNames.get)
                        if changedAnything:
                            se[:] = sorted(se, key=lambda x: x.tag) # Put masterLabel tag back at the end of the section.  We don't need to re-sort the fields because we might screw up where people were expecting to see them, if they weren't yet in alphabetical order.
                            root.set('xmlns','http://soap.sforce.com/2006/04/metadata')
                            if not os.path.exists(outpstr): os.makedirs(outpstr)
                            if not os.path.exists(outpstr+'reportTypes'): os.makedirs(outpstr+'reportTypes')
                            with open(outpstr+'reportTypes\\'+i, 'w', newline='') as fw:
                                dom_string = xml.dom.minidom.parseString(ElementTree.tostring(root)).toprettyxml(encoding='UTF-8', indent='    ')
                                dom_string = '\n'.join([s for s in dom_string.decode('UTF-8').splitlines() if s.strip()]) + '\n'
                                fw.write(dom_string)
                            changedThese.append(i[:-11])
if len(changedThese) > 0:
    pkgroot = ElementTree.Element('Package', attrib={'xmlns':'http://soap.sforce.com/2006/04/metadata'})
    typesElem = ElementTree.Element('types')
    for x in changedThese:
        membersElem = ElementTree.Element('members')
        membersElem.text = x
        typesElem.append(membersElem)
    namesElem = ElementTree.Element('name')
    namesElem.text = 'ReportType'
    typesElem.append(namesElem)
    pkgroot.append(typesElem)
    verElem = ElementTree.Element('version')
    verElem.text = metadataAPIVer
    pkgroot.append(verElem)
    with open(outpstr+'package.xml', 'w', newline='') as fw:
        dom_string = xml.dom.minidom.parseString(ElementTree.tostring(pkgroot)).toprettyxml(encoding='UTF-8', indent='    ')
        dom_string = '\n'.join([s for s in dom_string.decode('UTF-8').splitlines() if s.strip()]) + '\n'
        fw.write(dom_string)

print('all done')

Tuesday, April 4, 2017

Python for Salesforce Administrators - Introduction to XML and JSON

XML and JSON are like each other, but not like CSV

We've talked about how useful Python can be for processing table-style data stored in "CSV" plain-text files.

The key properties of table-style data are that:

  1. The "table" always has a certain number of columns
  2. Every single row in the table has the exact same set of "columns" (the exact same "keys") as every other row and as the table at large
  3. Every single row in the table is capable of having a value in each of these columns ... a "blank" placeholder still needs to be indicated if it doesn't.
  4. Every single row in the table can only have one value in each of these columns. (If you have only one "First Name" column, no row can have two first names.)
  5. Each conceptual "item" in the data is represented by a "row"
  6. Each conceptual "item" (row) can have no more than 1 level of "key-value" properties (a "column header" being the "key" and a given cell beneath a column header, in a specific row, being the "value").
    Example: There's no such thing as having the notion of a "Name" that breaks down at a lower level into "First Name" and "Last Name."
    Sure, the database you're exporting a "CSV" file from might have an automatically computed / "formula" field called "Name" that is just a space-separated merge of "First Name" and "Last Name." But the exported "CSV" file itself -- a plain-text representation of your data -- will show "Name," "First Name," and "Last Name" as independent "columns" whose values are at an equal "level" to each other.

There are other styles of data that can be stored in plain-text files as well.

The two main problems with table-style data that alternative textual representations of data try to get around are:

  1. Giving each conceptual "item" in the data key-value properties that are "nested" inside each other
  2. Letting each conceptual "item" in the data have "keys" that have nothing to do with the "keys" that other conceptual "items" in the data have

A plain-text file where punctuation indicates the start/end of each conceptual "item" in the data, and where the "keys" (and their values) inside of each conceptual "item" are also indicated by careful use of punctuation, can handle both of these requirements.

The two most common formats today are "XML" and "JSON." Plain-text exports of your current Salesforce configuration are often formatted in either of these styles.

  • In both formats, conceptual "items" can have more conceptual "items" nested inside of them.
  • In both formats, there is a way (in XML, 2) of defining the "keys and their values" possessed by each conceptual "item"
  • In XML, the word for the thing representing a conceptual "item" is an "element."
  • In JSON, the word for the thing representing a conceptual "item" is an "object."
  • Despite the "element" vs. "object" linguistic difference, XML and JSON represent the same type of data (nested data where each conceptual "item" gets to define its own "keys" and specify values for them).

We'll have a lot of examples in this post.

  • To view my XML examples graphically, paste them here and click "Tree View".
  • To view my JSON examples graphically, paste them here and click "Tree View".

XML

The punctuation that XML uses to define the beginning and end of an "element" is a "tagset." It looks like this:

<Person></Person>

As you can see, it's the same word, surrounded by less-than and greater-than signs, with the one indicating the "end" of the element starting with a forward-slash.

Each piece in the greater-than or less-than signs is considered a "tag," hence "tagset" for the notion of including them both (kind of like "parenthesis" versus "a set of parentheses").

The fact that the tagset exists in your text file means that it exists as a conceptual "item" in your data.

It doesn't matter whether or not there's anything typed between the tags (after the first greater-than, before the last less-than). This is now a conceptual item that "exists" in your data, simply because the tagset exists.

If it doesn't have anything between the tags, you can think of it a little like a row full of nothing but commas in a CSV file. It's there ... it's just blank.

(In fact, there's even a fancy shortcut for typing such tagsets: this single tag is equivalent to a tagset with nothing in the middle like the one above -- note the forward-slash before the greater-than sign:)

<Person/>

Note that already, though, our "blank" element has one big thing different about it than a row in a CSV file does: it has a name! Rows don't have names. We'll come back to this, but this is why XML has two ways of indicating an element's "keys and their values." By giving each element a name, XML allows the element itself to be used as a "key" definition for the larger context inside of which the element finds itself.

Here's an example:

<Shirt>
 <Color></Color>
 <Fabric></Fabric>
</Shirt>

There are 3 conceptual "items," or "elements," in this data, each of which has a name.

All 3 can stand alone as "elements" in the grammar of XML. Analogy:

You can write an English sentence that has multiple complete sentences inside of it; to write a sentence with multiple complete sentences inside, simply separate the two with semicolons.

However, the fact that the elements named "color" and "fabric" are nested between the tags of the element named "shirt" means that they are also indicating that this particular shirt has keys named "color" and "fabric" (the values to both of which are currently blank).

The line breaks and tabs aren't necessary in XML (even for saying where "color" stops and "fabric" begins), but they help humans read XML.

Now might be a good time to show you the other way of indicating that a particular shirt has a "color" and "fabric," but that their values are blank:

<Shirt Color="" Fabric=""></Shirt>
Or, in shortcut notation, since there's now nothing inside the "Shirt" tagset:
<Shirt Color="" Fabric=""/>

Note that this isn't always treated EXACTLY the same as our nested-tag example when it comes to programming languages that read XML. Some software might argue that there's more of a "nothingness" in the nested-tags example (it truly doesn't have a color), and there's more of a "value without any letters in it"-ness in the inside-the-Shirt-opening-tag example. Just sometimes, though, and that's often you, the programmer, deciding to make that distinction.

The big difference, though, is that in this case, "color" and "fabric" are not standalone elements.
They are "attributes" of the element named "Shirt".

You can't put more standalone "elements" between the quotes after the "=" of an "attribute." You're done. Only a plain-text value can go there.

You can't do this:

<Shirt Color="" Fabric="<Washable></Washable>"></Shirt>

But you can do this:

<Shirt>
 <Color></Color>
 <Fabric>
  <Washable></Washable>
 </Fabric>
</Shirt>

You also can't give any element more than one "attribute" of the same name, whereas you can nest as many same-named "elements" inside of an element as you like.

You can't do this:

<Shirt Color="" Color="" Fabric=""></Shirt>

But you can do this:

<Shirt>
 <Color></Color>
 <Color></Color>
 <Fabric>
  <Washable></Washable>
 </Fabric>
</Shirt>

Those are the main differences between the two ways XML gives you to define key-value pairs on an element.

  1. Attributes are, by definition, "the end of the line" when it comes to the "key" definitions attached to an element (and can't conflict with each others' names)
  2. The names of elements nested inside an element also serve as "key" definitions for the outer element, but they're "fuzzier" than attributes.
    This is usually considered a good thing.
    This desire for "fuzziness" & "repeatability" & "nested-ness" is one of the two reasons people choose a "nested" data format instead of a table-style format to represent their data.

A word of warning: this is also valid code:

<Shirt Color="" Fabric="">
 <Color></Color>
 <Color></Color>
 <Fabric>
  <Washable></Washable>
 </Fabric>
</Shirt>

A human might look at the shirt above and think it has 3 colors and 2 fabrics. It's probably better to think of it the way the computer thinks of it -- that the shirt above has 1 color attribute, 1 fabric attribute, 2 full-on elements nested within it each named "color," and 1 full-on element nested within it named "fabric."


Now let's give our shirt some "values!"

First of all, it's essential to remember that in a way, all these example' elements "keys" already had values. The values for the "keys" were just blank, or they were other elements**.

**(Think about the examples where an element named "washable" was nested inside of an element named "fabric" which was nested inside of an element named "shirt." The value for the "shirt" element's "fabric" key wasn't exactly blank -- the value for that key was more like: "an element called 'washable.'")

But when I say "values" for keys like "color" or "fabric" or "washable," you're probably thinking about things like the word "blue" or the word "red" or the word "leather" or the word "yes" or the word "no." So let's talk about those.

In XML, any given "element" can have exactly 0 or 1 plain-text "value" (like "leather" or "blue") between the tags that show where its boundaries are.
The only other thing that can go "inside" the element besides its (optional) plain-text "value" is more elements.

Here's a really simple element with a plain-text value:

<Shape>
 Rectangle
</Shape>

It's not common for the "outer-most" elements in an XML-formatted piece of data to have values--especially because valid XML always has just 1 outermost element. (If you don't care, you can just make up a name like "RootElement" for the tagset that holds all the "elements" you actually think of as your data.).

But even sometimes "2nd-outer-most" elements don't have values. Particularly when they represent some sort of abstract real-world object with a lot of complexity that you want to capture, they have 0 values but a lot of elements nested inside them.

Although you could describe a fleet of cars like this:

<RootElement>
 <Car>
  First car's Vehicle Identification Number here
 </Car>
 <Car>
  Second car's Vehicle Identification Number here
 </Car>
</RootElement>

The above code implies that the conceptual "items" that you've given names of "car" truly are their Vehicle Identification Numbers. Yet they're really not, are they? They're heavy chunks of steel taking up space in the real world. There isn't really a plain-text value that captures what they are. Therefore, you won't really see a lot of XML like that. Although the word "Car" is, technically, a "key" to each "element's" "value," in this case, it doesn't quite make sense to give "Car" a "value."

Here's a more realistic way of writing the data, using nested elements to show that each car has 1 "key" of "VIN" and that the "value" for that "VIN" key is filled in on both cars:

<RootElement>
 <Car>
  <VIN>
   First car's Vehicle Identification Number here
  </VIN>
 </Car>
 <Car>
  <VIN>
  Second car's Vehicle Identification Number here
  </VIN>
 </Car>
</RootElement>

Here's another realistic way of expressing the same idea, only using attributes to show each car's key & values:

<RootElement>
 <Car VIN="First car's Vehicle Identification Number here">
 </Car>
 <Car VIN="Second car's Vehicle Identification Number here">
 </Car>
</RootElement>

Or, for short (using attributes):

<RootElement>
 <Car VIN="First car's Vehicle Identification Number here"/>
 <Car VIN="Second car's Vehicle Identification Number here"/>
</RootElement>

In this little example, we're actually just dealing with multiple conceptual "items," each of which has the exact same keys as each other, and which has just 1 value per key, so remember that table-style (CSV) data could've easily represented the same data -- in this case, we've just got a 1-column CSV file:

"VIN"
"First car's Vehicle Identification Number here"
"Second car's Vehicle Identification Number here"

I digress -- but it's good to recognize what's going on in your data, and which types of plain-text files are capable of representing it.

Going back to our shirt example, let's say that our data set includes just 1 shirt, that it's "blue and red and green" and made out of "leather and cotton" and that the leather isn't washable but the cotton is.
Our XML representation of our data might look like this:

<Shirt>
 <Color>
  Blue
 </Color>
 <Color>
  Red
 </Color>
 <Color>
  Green
 </Color>
 <Fabric>
  Leather
  <Washable>
  No
  </Washable>
 </Fabric>
 <Fabric>
  Cotton
  <Washable>
  Yes
  </Washable>
 </Fabric>
</Shirt>

There isn't really a good way to represent that concept of what traits the shirt possesses in a single row of a CSV file, is there? This is where XML and JSON shine!

Read the XML above carefully. What you have is:

  • 1 element with a name of "shirt" that has 0 plain-text "values," but has 5 more elements nested inside of it
  • 3 elements with a name of "color" (nested inside the one named "shirt"), none of which have any elements nested inside of them, but each of which have 1 plain-text value
  • 2 elements with a name of "fabric" (nested inside the one named "shirt"), each of which have exactly 1 plain-text value, and each of which also have 1 more element nested inside of them
  • 2 elements with a name of "washable" (nested inside various ones named "fabric"), none of which have any elements nested inside of them, but each of which have 1 plain-text value

There also isn't really a good way to represent this shirt using "attributes" on the "shirt" tagset (because it has multiple colors and multiple fabrics, and because the fabrics have nested elements of their own). However, since each "fabric" only has exactly 1 "washable" key & value, you could use attributes for that as follows:

<Shirt>
 <Color>
  Blue
 </Color>
 <Color>
  Red
 </Color>
 <Color>
  Green
 </Color>
 <Fabric Washable="No">
  Leather
 </Fabric>
 <Fabric Washable="Yes">
  Cotton
 </Fabric>
</Shirt>

The choice is up to you, depending on which way you think it's easier to fetch/modify the values using code and which way you think it's easier for humans to read.

Another choice that's up to you is whether "Leather" is what the fabric truly is (the way "blue" is an adjective and therefore describes what the color truly is), or whether the notion of a fabric is too fuzzy in the real world to capture in a single word (like with our car) and should've been a key-value pair with a key like "name."
It's the same choice we had to make when deciding whether a car was its VIN or whether it had a VIN.
Outer-ward elements representing complex concepts usually just have key-value pairs (like with our car or our shirt examples).
For elements at "deeper" levels of nesting, you'll need to decide whether they "are" something (the optional 1 plain-text value they get) or whether they merely "have" things (nested elements & attributes).
That's a judgment call for you to make based on how your data is going to be used. All organization involves judgment calls trading flexibility against simplicity.
When it comes to writing software to process XML someone else already wrote, it's good to be able to recognize which judgment call they made (because the programming-language commands for extracting the two styles of writing key-value pairs are different).


JSON

The punctuation that JSON uses to define the beginning and end of an "object" is a set of "curly braces." It looks like this:

{}

  • Q: Hey waiddaminute -- that "object" doesn't have a name! I thought you said JSON "objects" and XML "elements" were pretty equivalent in terms of both being representations of conceptual "items" in the same style of organizing data!
  • A: Similar. But not the same. Good catch. JSON doesn't give objects a name. Nor do they get an optional "plain-text value" standing apart from anything nested inside of them. JSON objects look pretty different from XML elements.

Also, you can't just put JSON objects back-to-back the way you can put XML elements back-to-back; this isn't valid JSON:

{}
{}

Instead, you have to put them inside square-brackets and separate them with commas (remember not to put a comma after the last one, since nothing comes next--easy copy/paste mistake when you're putting each one on its own line). This is how you show 2 JSON objects at the same level as each other:

[
 {},
 {}
]

Note that the line breaks and tabs, however, are still for human benefit only.

Also, you don't have to include them inside any sort of "RootElement" container. That right there is valid JSON.

But getting back to our "JSON objects don't have names" problem ... what's the equivalent of this XML in JSON?

<Person/>

There isn't an exact translation, but one representation could be:

{
 "type": "Person"
}

In other words, you're making up an "attribute" (a "key") for the JSON object, calling it "type," and giving it a "value" of "Person" (yup, JSON objects have attributes, and like XML element attributes, you can only use a given attribute-name once!) You could have called it anything -- "type" is nothing special.

Similarly, either this XML:

<Shirt>
 <Color></Color>
 <Fabric></Fabric>
</Shirt>

Or this XML:

<Shirt Color="" Fabric=""></Shirt>

Might become this JSON:

{
 "type" : "Shirt",
 "Color" : null,
 "Fabric" : null
}

(Though, getting back to that thing I mentioned earlier about whether an empty tagset is somehow "emptier" than an empty attribute set of quotes, you might argue that the values of this JSON object's "Color" & "Fabric" attributes/keys should be two quotes in a row, rather than the special keyword 'null'. More than I want to get into right now, but software than processes JSON would see the two differently -- null is emptier than the empty-quotes.)

The biggest difference from XML that the lack of names in JSON introduces is this notion of having to make up your own keyword for the name if you really think it needs a name.

Also, as far as how-to-type-JSON, "attribute" names & values on a JSON object are separated from each other with a colon, and there needs to be a comma between attribute name-and-value pairs. (Again, don't forget not to put a comma after the last attribute name-value pair!)

Furthermore, attribute names are often inside quotes in JSON, and the value can be something that doesn't have quotes around it (we haven't gotten there yet).

Sometimes your data might not need names. If you have a bunch of conceptual "items" back-to-back at the same level of nesting, and none of them need a "plain-text value" representing what they truly are, and all of them just have key-value pairs describing what they "have," JSON is a lot shorter to type. (Especially if you take out all the tabs & line breaks I'm putting in to make this blog readable. JSON authors love to take out line breaks & tabs -- if you run into such code, paste it here and click "Beautify" to read it more easily ... just make sure the "code" you're punching in isn't confidential company information!) Consider this example.

Here's some XML representing a fleet of cars, each of which have different sets of key-value traits we care about tracking (we'll use "attribute" style and "tagset-with-nothing-inside shorthand here), but all of which are cars.

<RootElement>
 <Car color="blue" trim="chrome" trunk="hatchback"/>
 <Car appeal="sporty" doors="2"/>
 <Car doors="4" color="red" make="Ford"/>
</RootElement>

Maybe we already know they're all cars based on the context of our data. A JSON equivalent, without forcing each one to have a silly attribute like "type" (with a value of "car"), could be:

[
 {
  "color" : "blue",
  "trim" : "chrome",
  "trunk" : "hatchback"
 },
 {
  "appeal" : "sporty",
  "doors" : "2"
 },
 {
  "doors" : "4",
  "color" : "red",
  "make" : "Ford"
 }
]

So far, JSON doesn't look much more concise. But that note I made about "attribute values in JSON don't have to be in double-quotes" earlier is where its power really lies. Let's take a look at a shirt with two colors (blue, red) and 1 fabric (nylon). Here's the XML:

<Shirt>
 <Color>blue</Color>
 <Color>red</Color>
 <Fabric>nylon</Fabric>
</Shirt>

And here's some similar JSON (note that I didn't bother to force it to be called "shirt" and that I decided that saying "colors" would make more sense than "color"):

{
 "Colors" : ["blue","red"],
 "Fabric" : "nylon"
}

We're using the same square-brackets-and-commas notation to make a list out of "blue" and "red" that we used to put a bunch of JSON objects together into a data set full of cars. The entirety of the set of brackets is the value of this JSON object's attribute/key called "colors."

As you can see, XML and JSON get pretty different when it comes to writing down the fact that a conceptual "item" in your data set has multiple "keys" all with the same name, each with a different value.

  1. XML works like I just described it (multiple key-pair values, where the key names are the same as each other). JSON doesn't allow that. That's something special to XML. If you like that about XML, there you go--use XML. :-)
  2. In JSON, you just give your object one "key" (maybe making the name of that "key" a plural noun) and you shove all those values into a list.

So, to recap the kinds of "value" you can give an attribute/"key" belonging to a JSON "object" (conceptual item):

  • We've seen that the "value" for a given attribute/"key" of a JSON object can be a piece of plain text (put it between double-quotes).
  • We've seen it be the keyword 'null' (no quotes around it).
  • We've seen it be a list (square-brackets, with commas separating multiple values).
    Note that you can certainly just have one value inside the square brackets, or they can even be empty if the list is empty. Just keep in mind that it's still a list of values to a computer, even if it's empty or a list of size 1.
  • We're about to see that it can be another JSON "object" (we're about to nest things, just like we did in XML!)

Let's go back to our data set that includes just 1 shirt, which is "blue and red and green" and made out of "leather and cotton," where the leather isn't washable but the cotton is.
A JSON representation of our data might look like this:

{
 "type" : "Shirt",
 "colors" : ["blue","red","green"],
 "fabrics" :
  [
   {
    "type" : "Leather",
    "washable" : "No"
   },
   {
    "type" : "Cotton",
    "washable" : "Yes"
   }
  ]
}

As a reminder, here was a short XML version of the same shirt:

<Shirt>
 <Color>
  Blue
 </Color>
 <Color>
  Red
 </Color>
 <Color>
  Green
 </Color>
 <Fabric Washable="No">
  Leather
 </Fabric>
 <Fabric Washable="Yes">
  Cotton
 </Fabric>
</Shirt>

What I notice the most is:

  • JSON is awkward when conceptual "items" in our data need to have attributes and a meaningful name of their own (like "shirt" and "leather" and "cotton")
  • XML is overly wordy by making conceptual "items" out of things that already were true "attributes" of what we humans really think of as our data's conceptual "items" (like "color" -- we're probably not really thinking of it as an "item" in our data set, because "color" is no more complicated than its value(s) -- it doesn't have any other traits about the color that we need to describe).

What's In It For You?

In the end, as a Salesforce administrator, what matters most is being able to recognize which kind of data the Salesforce servers have given you (or in which format the servers expect to receive data from you).

You don't exactly get to argue with Salesforce about which format they should have picked.

  • When you're picking apart data Salesforce sent you, you just need to know how to "parse" the file to extract the data of interest to you.
  • When you're composing a file to send Salesforce, you'll model it after an existing file they sent you (or technical documentation) to figure out exactly how they want you to arrange the details.

In future posts, we'll talk about writing Python code that can do both of these tasks.

  • Understanding the relationship between the textual representation of the data and what it means will be crucial to those exercises.
  • Understanding when you're looking at data that can be made "flat" and "consistent from one conceptual item in the data to the next" (that is, when you can interpret it like a table/CSV) and when you're looking at data that really doesn't have those characteristics is crucial as well. Remember how I "diverted" and pointed out that our fleet of cars with nothing but VINs could have just as easily been a 1-column CSV file? Ideally, you want to be able to make that kind of commentary, too, about entire XML/JSON files or about fragments of them.
    Remember to play with the graphical viewers (XML, JSON)!

Hopefully, this blog post will help you with both tasks by better understanding what the example data says and how it's shaped when you see it.


Table of Contents

Friday, April 1, 2016

Creating Salesforce custom objects and custom fields with code (the Metadata API)

Thanks to StackExchange, I got the help I needed when I last posted about creating custom objects and fields with code.

My department at a university bought a Salesforce org 5 years ago. That's the one I'm the sysadmin for. We meant it to fill a lot of gaps left for our department by the university's central ERP database (Banner) - such as Banner not handling week-long "courses" very well, Banner not handling "deans' corporate rolodexes" very well, Banner not playing well with modern mass-emailing/texting/etc. software, etc.

Our university's central IT department just bought a Salesforce org about 1 year ago. We're not sure yet exactly how widely it will be used, but for starters, it's replacing our home-coded web-based application system (which used to dump straight into our ERP).

Because of that limited scope, it really doesn't yet fill any of the "holes" in the central ERP's offerings that my department bought its own Salesforce org for. Which means we're leaving both of them live.

And now instead of just worrying about integrating a daily dump from the ERP into our Salesforce org, I also get to set up a daily dump from the new central-Salesforce into our Salesforce.


Our typical pattern for "daily dumps" is "dump the data where it can't hurt anything, and move it where it belongs by trigger/workflow/process afterwards."

So, for example, "First Name" from Banner dumps into Contact.Banner_First_Name__c - and then is post-processed to fill out Contact.FirstName if there isn't already something there. (List View reports help us find & correct cases where the two fields have different values.)

I was about to dump 20 never-before-dumped fields' worth of data into Contact and about 6 objects (at 4-20 fields apiece) from yet another database into our Salesforce, so I had a lot of "creating objects & fields" to do before I could even get started setting up the Jitterbit feed between them.

What really bugged me was the idea that because I was essentially creating mirror images of another Salesforce org's data, and because it was so easy to get XML copies of the official definitions of those fields as they existed in the "source" org, there had to be a way to use code to build those fields in the "target" org. I wanted to use code and a text editor because I wasn't trying to completely replicate the other Salesforce org - I had some "pruning" to do to get down to those 100-some items to create in my org and strip them of any inter-dependencies I didn't intend to mirror.

Indeed, there is a way. Here's how I did it.


Firstly, every time I needed the XML definition of objects & their custom fields from the central-university Salesforce org, I built an XML file called package.xml that looked something like this, uploaded it to Workbench while logged into that org, and saved the "{ObjectAPIName}.object" files in the "objects" folder in the resulting ZIP file.

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <fullName>GetObjectsPackage</fullName>
    <types>
        <members>Contact</members>
  <members>Application__c</members>
  <members>Test_Score__c</members>
  <members>App_Checklist_Document__c</members>
  <members>Recommendation__c</members>
  <members>Previous_Education__c</members>
  <members>Work_History__c</members>
        <name>CustomObject</name>
    </types>
    <version>36.0</version>
</Package>

Next, I wanted to throw away all the "junk" in those "{ObjectAPIName}.object" files from the central-university Salesforce org and extract only the contents of their "<fields>" tags.

The following Python code loops through a folder full of "{ObjectAPIName}.object" files and creates an "extracts" subfolder containing "{ObjectAPIName}-fields.xml" files showing just the fields I'm interested in (defined in ordinary string lists at the top of the code).

import os, fnmatch
from xml.dom import minidom

dumppath = 'C:\\SOMEPATH\\dumpeddata\\'
objapis = [f.rstrip('object').rstrip('.') for f in fnmatch.filter(os.listdir(dumppath), '*.object')]

objFieldsOfInterest = {
                        'Contact' : ['FirstName','LastName','Birthdate','Citizenship__c'],
                        'Application__c' : ['AppDate__c','AppStatus__c'],
                        'Test_Score__c' : ['Test_Score__c','Test_Type__c','Test_Date__c'],
                        'App_Checklist_Document__c' : ['Document_Type__c','Received_Date__c'],
                        'Recommendation__c' : ['Recommender_Last_Name__c','Recommender_First_Name__c','Relationship__c','Document_Status__c'],
                        'Previous_Education__c' : ['Document_Status__c','School_Name__c','Level__c','Graduation_Date__c'],
                        'Work_History__c' : ['Employer__c','Job_Title__c','Start_Date__c','End_Date__c']
                        }

objsAndDumpedXMLFields = {}
objsAndDumpedFieldNames = {}
objsAndXMLFieldsOfInterest = {}

# Fill the outer-level "object" dicts with XML and strings + write the data to disk
for o in objapis:
    objsAndDumpedXMLFields[o] = minidom.parse(dumppath+o+'.object').getElementsByTagName('fields')
    objsAndDumpedFieldNames[o] = [x.getElementsByTagName('fullName')[0].firstChild.nodeValue for x in objsAndDumpedXMLFields[o]]
    for f in objsAndDumpedXMLFields[o]:
        if o in objFieldsOfInterest and f.getElementsByTagName('fullName')[0].firstChild.nodeValue in objFieldsOfInterest[o]:
            #print(f.getElementsByTagName('fullName')[0].firstChild.nodeValue)
            if o not in objsAndXMLFieldsOfInterest:
                objsAndXMLFieldsOfInterest[o] = []
            objsAndXMLFieldsOfInterest[o].append(f)

# Write "objsAndXMLFieldsOfInterest" out to temporary files
if len(objsAndXMLFieldsOfInterest) > 0:
    if not os.path.exists(dumppath+'extracts\\'): os.makedirs(dumppath+'extracts\\')
    for filteredo in objsAndXMLFieldsOfInterest:
        with open(dumppath+'extracts\\'+filteredo+'-fields.xml', 'w') as outfile:
            xmlstrlist = []
            for elem in objsAndXMLFieldsOfInterest[filteredo]:
                xmlstrlist.append(elem.toxml())
            outfile.write('<?xml version="1.0" encoding="UTF-8"?>'+'\n'+'<rootnode>'+'\n'+'    '+'\n    '.join(xmlstrlist)+'\n'+'</rootnode>')

Cobbling together a folder full of "{ObjectAPIName}.object" files to upload into our Salesforce org was a pretty manual job in Notepad++.

I think I actually built the 6 custom objects by hand using Salesforce's normal web interface, then exported those ".object" files using Workbench as described above, and stripped pretty much all XML out of them (leaving only a few tags that seemed to be mandatory, such as "<sharingModel>," "<label>," "<nameField>," & "<pluralLabel>").

Then I re-filled those skeletons with the "<fields>" tags I'd extracted in the previous step (sometimes with some data altered - for example, I turned big picklists of countries into 100-character text fields to avoid having to keep picklists in sync, and I renamed "Contact.FirstName" from the extracted fields to "Contact.OtherDatabase_First_Name__c" for upload into our org).


Once I'd built such a folder full of "{ObjectAPIName}.object" files to upload into our Salesforce org, I had to put them inside a properly-formatted "package" that also included permissions for the new objects & fields.

To make this "package," I ran the following Python code (note that my permissions are pretty simple - you might need more code for more complex permissions):

import os
from xml.dom import minidom
from xml.etree.ElementTree import Element, SubElement, tostring
import itertools


oldobjectxmlspath = 'C:\\WhereIWasBuildingMyObjectFiles\\'

pkgpath = 'C:\\SomePath\\mynewpackage\\'
pkgfn = 'package.xml'
objpath = pkgpath+'objects\\'
prfpath = pkgpath+'profiles\\'
profiletypeswhocanediteverything = ['Admin','Admissions','Records Management']

objapis = None # Not really necessary, but this helps me keep track of outer-level variables
objswithfieldnames = {}
numobjswithfieldnames = 0
pkgroot = None

# Make the paths if they don't exist
if not os.path.exists(pkgpath): os.makedirs(pkgpath)
if not os.path.exists(objpath): os.makedirs(objpath)
if not os.path.exists(prfpath): os.makedirs(prfpath)

# Copy in any data we want to work with (COMMENT THIS OUT IF DATA IS ALREADY IN PLACE)
from shutil import copyfile
for i in os.listdir(oldobjectxmlspath):
    if not os.path.exists(objpath+i): copyfile(oldobjectxmlspath+i, objpath+i)

# Set "objapis" outer-level variable
objapis = [f.rstrip('object').rstrip('.') for f in os.listdir(objpath)] # Not sure why I can't just strip '.object'

# Set "objfieldnamesdict" outer-level variable
for f in objapis:
    objfields = [f.getElementsByTagName('fullName')[0].firstChild.nodeValue for f in minidom.parse(objpath+f+'.object').getElementsByTagName('fields')]
    if len(objfields) > 0:
        objswithfieldnames[f] = objfields

# Set numobjswithfieldnames outer-level variable for quick-ref later
numobjswithfieldnames = len(list(itertools.chain.from_iterable(objswithfieldnames.values())))


# Make our own "prettify" function since our IDE doesn't seem to have ElementPrettify in it
def prettify(elem):
    #Return a pretty-printed XML string for the Element.
    rough_string = tostring(elem, 'utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent="\t")

# Build the XML for "package.xml" & for the can-edit-all-fields ".profile" files and write them to disk
if len(objapis) > 0:
    # "package.xml" file
    pkgroot = Element('Package')
    pkgroot.set('xmlns', 'http://soap.sforce.com/2006/04/metadata')
    SubElement(pkgroot, 'fullName').text = 'PyPkg'
    objTypesSE = SubElement(pkgroot, 'types')
    for o in objapis:
        SubElement(objTypesSE, 'members').text = o
    SubElement(objTypesSE, 'name').text = 'CustomObject'
    if numobjswithfieldnames > 0:
        fieldTypesSE = SubElement(pkgroot, 'types')
        for o in objswithfieldnames:
            for f in objswithfieldnames[o]:
                SubElement(fieldTypesSE, 'members').text = o+'.'+f
        SubElement(fieldTypesSE, 'name').text = 'CustomField'
    if len(profiletypeswhocanediteverything) > 0:
        profTypesSE = SubElement(pkgroot, 'types')
        for p in profiletypeswhocanediteverything:
            SubElement(profTypesSE, 'members').text = p
        SubElement(profTypesSE, 'name').text = 'Profile'
    SubElement(pkgroot, 'version').text = '36.0'
    with open(pkgpath+pkgfn, 'w') as pkgfile:
        pkgfile.write(prettify(pkgroot))
    
    # "profile\_____.profile" files for profiles who can edit all the fields in question:
    if len(profiletypeswhocanediteverything) > 0:
        for p in profiletypeswhocanediteverything:
            pfroot = Element('Profile')
            pfroot.set('xmlns', 'http://soap.sforce.com/2006/04/metadata')
            if numobjswithfieldnames > 0:
                for o in objswithfieldnames:
                    for f in objswithfieldnames[o]:
                        fieldPermSE = SubElement(pfroot, 'fieldPermissions')
                        SubElement(fieldPermSE, 'editable').text = 'true'
                        SubElement(fieldPermSE, 'field').text = o+'.'+f
                        SubElement(fieldPermSE, 'hidden').text = 'false'
                        SubElement(fieldPermSE, 'readable').text = 'true'
            for o in objapis:
                oPermSE = SubElement(pfroot, 'objectPermissions')
                SubElement(oPermSE, 'allowCreate').text = 'true'
                SubElement(oPermSE, 'allowDelete').text = 'true'
                SubElement(oPermSE, 'allowEdit').text = 'true'
                SubElement(oPermSE, 'allowRead').text = 'true'
                SubElement(oPermSE, 'modifyAllRecords').text = 'true'
                SubElement(oPermSE, 'object').text = o
                SubElement(oPermSE, 'viewAllRecords').text = 'true'
            with open(prfpath+p+'.profile', 'w') as pffile:
                pffile.write(prettify(pfroot))

But I wasn't quite done building the package after I ran the Python.

When the package includes objects that have a master-detail relationship to another object, the "detail" object's field pointing to the "master" MUST be included in its ".object" file and in "package.xml," but MUST NOT be mentioned in the ".profile" files. So next, I had to go through the contents of "C:\SomePath\mynewpackage\profile\" and strip the "<fieldPermissions>...</fieldPermissions>" blocks for those fields out of the ".profile" files. (I just did this for one ".profile" file in Notepad++, then copied/pasted the file's entire contents into the other ".profile" files, since they were all identical except in filename.)

I then zipped up "C:\SomePath\mynewpackage" into "mynewpackage.zip," went to Workbench while logged into our Salesforce org, and uploaded the package.
(It's best to turn on "Check Only" at first, and deploys to production orgs also require "Rollback On Error" checked and "Run Tests" to be set to "RunLocalTests".)

That's it! Workbench will tell you how the data push to your org went, and if things went well, go browse around your object definitions in the normal Salesforce web interface - everything should be there! You'll still have to add the fields to page layouts and such by hand (each new custom object will have a mostly-blank default page layout), but they will be there, visible to Jitterbit, data loading tools, Workbench, etc. (as long as it's logged in as someone who has permission to the objects/fields).


Finally, a few lessons I learned along the way in building my "packages" are:

  1. If I include an object in a package and simply don't mention its existing fields, they will be left alone.
    • This means I can say, "Oops, I forgot those 5 fields!" later on and build a whole package just around adding them to our Salesforce org. It's faster than building those 5 fields by hand once all this infrastructure is in place.
  2. If I include a profile in a package and simply don't mention its permissions on objects/fields, they will be left alone.
  3. If an object/field already exists (matching via API name), anything that conflicts with its existing settings that I put into the package will overwrite that object's/field's settings if possible, and error out if not.
  4. It's hard to write perfect Salesforce-Metadata-API-package XML the first time, so you might want a faster way to repeatedly "deploy" your packages (after getting error messages) than ZIPping+Workbench provides. Read more here about installing and using a tool for this purpose called ANT.