Package: dhis2.py
A Python library forDHIS2wrappingrequestsforgeneral-purpose API interactionwith DHIS2. It attempts to beuseful for any data/metadata import and export tasksincluding various utilities like file loading, UID generation and logging. A strong focus is on JSON.Supported and tested on Linux/macOS, Windows and DHIS2 versions >= 2.25. Python 3.6+ is required.Contents1Installation2Quickstart3Usage3.1Api instance creation3.1.1Authentication in code3.1.2Authentication from file3.1.3Get info about the DHIS2 instance3.2Getting things3.2.1Paging3.2.2SQL Views3.2.3GET other content types3.3Updating / deleting things3.3.1Post partitioned payloads3.4Multiple params with same key3.5Utilities3.5.1Load JSON file3.5.2Load CSV file3.5.3Generate UID3.5.4Validate UID3.5.5Clean an object3.5.6Print pretty JSON3.5.7Check import response3.6Logging3.7Exceptions4Examples5Changelog6Contribute7License1InstallationPython 3.6+ is required.pipinstalldhis2.pyFor instructions on installing Python / pip for your operating system seerealpython.com/installing-python.2QuickstartCreate anApiobject:fromdhis2importApiapi=Api('play.dhis2.org/demo','admin','district')Then run requests on it:r=api.get('organisationUnits/Rp268JB6Ne4',params={'fields':'id,name'})print(r.json())# { "name": "Adonkia CHP", "id": "Rp268JB6Ne4" }r=api.post('metadata',json={'dataElements':[...]})print(r.status_code)# 200api.get()api.post()api.put()api.patch()api.delete()see below for more methods.They all return aResponseobject fromrequestsexcept noted otherwise. This means methods and attributes are equally available
(e.g.Response.url,Response.text,Response.status_codeetc.).3Usage3.1Api instance creation3.1.1Authentication in codeCreate an API objectfromdhis2importApiapi=Api('play.dhis2.org/demo','admin','district')optional arguments:api_version: DHIS2 API versionuser_agent: submit your own User-Agent header. This is useful if you need to parse e.g. Nginx logs later.3.1.2Authentication from fileLoad from a auth JSON file in order to not store credentials in scripts.
Must have the following structure:{"dhis":{"baseurl":"http://localhost:8080","username":"admin","password":"district"}}fromdhis2importApiapi=Api.from_auth_file('path/to/auth.json',api_version=29,user_agent='myApp/1.0')If no file path is specified, it tries to find a file calleddish.jsonin:theDHIS_HOMEenvironment variableyour Home folder3.1.3Get info about the DHIS2 instanceAPI version as a string:print(api.version)# '2.30'API version as an integer:print(api.version_int)# 30API revision / build:print(api.revision)# '17f7f0b'API URL:print(api.api_url)# 'https://play.dhis2.org/demo/api/30'Base URL:print(api.base_url)# 'https://play.dhis2.org/demo'system info (this is persisted across the session):print(api.info)# {#   "lastAnalyticsTableRuntime": "11 m, 51 s",#   "systemId": "eed3d451-4ff5-4193-b951-ffcc68954299",#   "contextPath": "https://play.dhis2.org/2.30",#   ...3.2Getting thingsNormal method:api.get(), e.g.r=api.get('organisationUnits/Rp268JB6Ne4',params={'fields':'id,name'})data=r.json()Parameters:timeout: to override the timeout value (default: 5 seconds) in order to prevent the client to wait indefinitely on a server response.3.2.1PagingPaging for larger GET requests viaapi.get_paged()Two possible ways:Process every page as they come in:forpageinapi.get_paged('organisationUnits',page_size=100):print(page)# { "organisationUnits": [ {...}, {...} ] } (100 organisationUnits)Load all pages before proceeding (this may take a long time) - to do this, do not useforand addmerge=True:all_pages=api.get_paged('organisationUnits',page_size=100,merge=True):print(all_pages)# { "organisationUnits": [ {...}, {...} ] } (all organisationUnits)Note:Returns directly a JSON object, not a requests.Response object unlike normal GETs.3.2.2SQL ViewsGet SQL View data as if you’d open a CSV file, optimized for larger payloads, viaapi.get_sqlview()# poll a sqlView of type VIEW or MATERIALIZED_VIEW:forrowinapi.get_sqlview('YOaOY605rzh',execute=True,criteria={'name':'0-11m'}):print(row)# {'code': 'COC_358963', 'name': '0-11m'}# similarly, poll a sqlView of type QUERY:forrowinapi.get_sqlview('qMYMT0iUGkG',var={'valueType':'INTEGER'}):print(row)# if you want a list directly, cast it to a ``list`` or add ``merge=True``:data=list(api.get_sqlview('qMYMT0iUGkG',var={'valueType':'INTEGER'}))# OR# data = api.get_sqlview('qMYMT0iUGkG', var={'valueType': 'INTEGER'}, merge=True)Note:Returns directly a JSON object, not a requests.response object unlike normal GETs.Beginning of 2.26 you can also use normal filtering on sqlViews. In that case, it’s recommended
to use thestream=Trueparameter of theDhis.get()method.3.2.3GET other content typesUsually defaults to JSON but you can get other file types:r=api.get('organisationUnits/Rp268JB6Ne4',file_type='xml')print(r.text)# <?xml version='1.0' encoding='UTF-8'?><organisationUnit ...r=api.get('organisationUnits/Rp268JB6Ne4',file_type='pdf')withopen('/path/to/file.pdf','wb')asf:f.write(r.content)3.3Updating / deleting thingsNormal methods:api.post()api.put()api.patch()api.delete()3.3.1Post partitioned payloadsIf you have such a large payload (e.g. metadata imports) that you frequently get a HTTP Error:413 Request Entity Too Largeresponse e.g. from Nginx you might benefit from using
the following method that splits your payload in partitions / chunks and posts them one-by-one.
You define the amount of elements in each POST by specifying a number inthresh(default:1000).Note that it is only possible to submit one key per payload (e.g.dataElementsonly, not additionallyorganisationUnitsin the same payload).api.post_partitioned()importjsondata={"organisationUnits":[{...},{...}# very large number of org units]{forresponseinapi.post_partitioned('metadata',json=data,thresh=5000):text=json.loads(response.text)print('[{}] -{}'.format(text['status'],json.dumps(text['stats'])))3.4Multiple params with same keyIf you need to pass multiple parameters to your request with the same key, you may submit as a list of tuples instead when e.g.:r=api.get('dataValueSets',params=[('dataSet','pBOMPrpg1QX'),('dataSet','BfMAe6Itzgt'),('orgUnit','YuQRtpLP10I'),('orgUnit','vWbkYPRmKyS'),('startDate','2013-01-01'),('endDate','2013-01-31')])alternatively:r=api.get('dataValueSets',params={'dataSet':['pBOMPrpg1QX','BfMAe6Itzgt'],'orgUnit':['YuQRtpLP10I','vWbkYPRmKyS'],'startDate':'2013-01-01','endDate':'2013-01-31'})3.5Utilities3.5.1Load JSON filefromdhis2importload_jsonjson_data=load_json('/path/to/file.json')print(json_data)# { "id": ... }3.5.2Load CSV fileVia a Python generator:fromdhis2importload_csvforrowinload_csv('/path/to/file.csv'):print(row)# { "id": ... }Via a normal list, loaded fully into memory:data=list(load_csv('/path/to/file.csv'))3.5.3Generate UIDCreate a DHIS2 UID:uid=generate_uid()print(uid)# 'Rp268JB6Ne4'To create a list of 1000 UIDs:uids=[generate_uid()for_inrange(1000)]3.5.4Validate UIDCheck if something is a valid DHIS2 UID:uid='MmwcGkxy876'print(is_valid_uid(uid))# Trueuid=25329print(is_valid_uid(uid))# Falseuid='MmwcGkxy876 'print(is_valid_uid(uid))# False3.5.5Clean an objectUseful for deep-removing certain keys in an object,
e.g. remove all sharing by recursively removing alluseranduserGroupAccessesfields.fromdhis2importclean_objmetadata={"dataElements":[{"name":"ANC 1st visit","id":"fbfJHSPpUQD","publicAccess":"rw------","userGroupAccesses":[{"access":"r-r-----","userGroupUid":"Rg8wusV7QYi","displayName":"HIV Program Coordinators","id":"Rg8wusV7QYi"},{"access":"rwr-----","userGroupUid":"qMjBflJMOfB","displayName":"Family Planning Program","id":"qMjBflJMOfB"}]}],"dataSets":[{"name":"ART monthly summary","id":"lyLU2wR22tC","publicAccess":"rwr-----","userGroupAccesses":[{"access":"r-rw----","userGroupUid":"GogLpGmkL0g","displayName":"_DATASET_Child Health Program Manager","id":"GogLpGmkL0g"}]}]}cleaned=clean_obj(metadata,['userGroupAccesses','publicAccess'])pretty_json(cleaned)Which would eventually recursively remove all keys matching touserGroupAccessesorpublicAccess:{"dataElements":[{"name":"ANC 1st visit","id":"fbfJHSPpUQD"}],"dataSets":[{"name":"ART monthly summary","id":"lyLU2wR22tC"}]}3.5.6Print pretty JSONPrint easy-readable JSON objects with colors, utilizesPygments.fromdhis2importpretty_jsonobj={"dataElements":[{"name":"Accute Flaccid Paralysis (Deaths < 5 yrs)","id":"FTRrcoaog83","aggregationType":"SUM"}]}pretty_json(obj)… prints (in a terminal it will have colors):{"dataElements":[{"aggregationType":"SUM","id":"FTRrcoaog83","name":"Accute Flaccid Paralysis (Deaths < 5 yrs)"}]}3.5.7Check import responseCheck the importSummary response from e.g./api/dataValuesor/api/metadataimport. Returns true if import went well,
false if there are ignored values or the status reports not a OK or SUCCESS.
This can be useful if the response returns a 200 OK but the summary reports ignored data.fromdhis2importimport_response_ok# response as e.g. from response = api.post('metadata', data=payload).json()response={"description":"The import process failed: java.lang.String cannot be cast to java.lang.Boolean","importCount":{"deleted":0,"ignored":1,"imported":0,"updated":0},"responseType":"ImportSummary","status":"WARNING"}import_successful=import_response_ok(response)print(import_successful)# False3.6LoggingLogging utilizeslogzero.Color output depending on log levelDHIS2 log format including the line of the calleroptionallogfile=specifies a rotating log file path (20 x 10MB files)fromdhis2importsetup_logger,loggersetup_logger(logfile='/var/log/app.log')logger.info('my log message')logger.warning('missing something')logger.error('something went wrong')logger.exception('with stacktrace')* INFO  2018-06-01 18:19:40,001  my log message [script:86]
* ERROR  2018-06-01 18:19:40,007  something went wrong [script:87]Usesetup_logger(include_caller=False)if you want to remove[script:86]from logs.3.7ExceptionsThere are two exceptions:RequestException: DHIS2 didn’t like what you requested. See the exception’scode,urlanddescription.ClientException: Something didn’t work with the client not involving DHIS2.They both inherit fromDhis2PyException.4ExamplesReal-world script examples can be found in theexamplesfolder.dhis2.py is used indhis2-pk(dhis2-pocket-knife)5ChangelogVersionschangelog6ContributeFeedback welcome!AddissueInstall the dev environment (see below)Fork, add changes tomasterbranch, ensure tests pass with full coverage and add a Pull Requestpipinstallpipenvgitclonehttps://github.com/davidhuser/dhis2.pycddhis2.pypipenvinstall--devpipenvruntests# install pre-commit hookspipenvrunpre-commitinstall# run type annotation checkpipenvrunmypydhis2# run flake8 style guide enforcementpipenvrunflake87Licensedhis2.py’s source is provided under MIT license.
See LICENCE for details.Copyright (c), 2020, David Huser