{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false, "tags": [ "remove-cell" ] }, "outputs": [], "source": [ "#|hide\n", "#|default_exp reports" ] }, { "cell_type": "markdown", "metadata": { "collapsed": false }, "source": [ "# Maintenance Reports\n", "Parse Maintenance reports from LimeSurvey and legacy spreadsheet formats.\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false }, "outputs": [], "source": [ "#|export\n", "import pandas as pd\n", "from pandas._typing import (\n", " FilePath,\n", " ReadCsvBuffer,\n", ")\n", "import datetime as dt\n", "import numpy as np\n", "from toolz import assoc_in\n", "\n", "from pyrnet import utils" ] }, { "cell_type": "markdown", "metadata": { "collapsed": false }, "source": [ "\n", "## Survey Export\n", "In the following, the functions are designed to work with the survey response export in the .csv format:\n", "* Field separator: \"Semicolon\"\n", "* Responses: \"Answer codes\"\n", "* Headings: \"Question code\"\n", "\n", "The responses can be exported manually from the website ..." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idsubmitdatelastpagestartlanguageseedstartdatedatestampQ00Q01MainQ01...interviewtimegroupTime57Q00TimeQ01TimegroupTime59MainQ01TimeMainQ02TimegroupTime58ExtraQ01TimeExtraQ02Time
012023-05-08 15:13:331en14289825182023-05-08 15:02:532023-05-08 15:13:331this is a testAO01...837.45NoneNoneNoneNoneNoneNoneNoneNoneNone
122023-05-08 16:08:201en8528616592023-05-08 16:08:092023-05-08 16:08:202222AO03...11.13NoneNoneNoneNoneNoneNoneNoneNoneNone
232023-05-08 16:09:061en6328787302023-05-08 16:08:462023-05-08 16:09:061222AO02...19.89NoneNoneNoneNoneNoneNoneNoneNoneNone
\n", "

3 rows × 27 columns

\n", "
" ], "text/plain": [ " id submitdate lastpage startlanguage seed \\\n", "0 1 2023-05-08 15:13:33 1 en 1428982518 \n", "1 2 2023-05-08 16:08:20 1 en 852861659 \n", "2 3 2023-05-08 16:09:06 1 en 632878730 \n", "\n", " startdate datestamp Q00 Q01 MainQ01 ... \\\n", "0 2023-05-08 15:02:53 2023-05-08 15:13:33 1 this is a test AO01 ... \n", "1 2023-05-08 16:08:09 2023-05-08 16:08:20 2 222 AO03 ... \n", "2 2023-05-08 16:08:46 2023-05-08 16:09:06 1 222 AO02 ... \n", "\n", " interviewtime groupTime57 Q00Time Q01Time groupTime59 MainQ01Time \\\n", "0 837.45 None None None None None \n", "1 11.13 None None None None None \n", "2 19.89 None None None None None \n", "\n", " MainQ02Time groupTime58 ExtraQ01Time ExtraQ02Time \n", "0 None None None None \n", "1 None None None None \n", "2 None None None None \n", "\n", "[3 rows x 27 columns]" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fn = \"../../example_data/results-survey224783.csv\"\n", "\n", "df = pd.read_csv(fn, sep=';')\n", "df = df.fillna(\"None\")\n", "df\n" ] }, { "cell_type": "markdown", "metadata": { "collapsed": false }, "source": [ "... or via the *limepy* python package." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import limepy\n", "import getpass\n", "from io import StringIO\n", "\n", "pwd = getpass.getpass(\"LimeSurvey BeRichter Password: \")\n", "if pwd != '':\n", " url = \"https://lgs-car.limesurvey.net/admin/remotecontrol\"\n", " csv = limepy.download.get_responses(\n", " base_url=url,\n", " user_name=\"BeRichter\",\n", " password=pwd,\n", " user_id=1,\n", " sid=224783\n", " )\n", " df = pd.read_csv(StringIO(csv), sep=';')\n", " df = df.fillna(\"None\")\n", " df\n", "else:\n", " print(\"No password, no data.\")" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [], "source": [ "#|export\n", "def get_responses(\n", " *,\n", " fn: FilePath | ReadCsvBuffer[bytes] | ReadCsvBuffer[str]|None = None,\n", " online: dict|None = None\n", ") -> pd.DataFrame:\n", " \"\"\"\n", " Get LimeSurvey responses as pandas Dataframe providing a file or online download information.\n", "\n", " Parameters\n", " ----------\n", " fn: str, path object or file-like object\n", " Any pandas readable representation of the LimeSurvey response export file\n", " (.csv, sep=;, answer and question codes).\n", " online: dict\n", " Dictionary of information required to download the responses via limepy:\n", " * base_url -> limesurvey remote_control url\n", " * user_name -> account name\n", " * password\n", " * user_id -> ID of account user (usually 1)\n", " * sid -> Survey ID\n", "\n", " Minimal information stored in *online* is the base_url, other information will then be filled via user input promt.\n", "\n", " Returns\n", " -------\n", " pd.Dataframe\n", " parsed responses csv file\n", " \"\"\"\n", " if fn is not None:\n", " # legacy support:\n", " if fn.endswith(\".xls\") or fn.endswith(\".xlsx\"):\n", " return parse_legacy_logbook(fn)\n", " filepath_or_buffer = fn\n", " elif online is not None:\n", " import limepy\n", " import getpass\n", " from io import StringIO\n", " if \"base_url\" not in online:\n", " raise ValueError\n", " if \"user_name\" not in online:\n", " online.update({'user_name': input(\"LimeSurvey Account Name: \")})\n", " if \"password\" not in online:\n", " online.update({'password': getpass.getpass(\"LimeSurvey Password: \")})\n", " if \"user_id\" not in online:\n", " online.update({\"user_id\": input(\"LimeSurvey User ID: \")})\n", " if \"sid\" not in online:\n", " online.update({'sid': input(\"LimeSurvey Survey ID: \")})\n", "\n", " csv = limepy.download.get_responses(**online)\n", " filepath_or_buffer = StringIO(csv)\n", " else:\n", " raise ValueError\n", " df = pd.read_csv(filepath_or_buffer, sep=';')\n", " df = df.fillna(\"None\")\n", " return df" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idsubmitdatelastpagestartlanguageseedstartdatedatestampQ00Q01MainQ01MainQ01[comment]MainQ02MainQ02[comment]ExtraQ01ExtraQ01[comment]ExtraQ02ExtraQ02[comment]
012023-05-08 15:13:331en14289825182023-05-08 15:02:532023-05-08 15:13:331this is a testAO01NoneAO02testing notesAO03NoneAO01None
122023-05-08 16:08:201en8528616592023-05-08 16:08:092023-05-08 16:08:202222AO03NoneNoneNoneNoneNoneNoneNone
232023-05-08 16:09:061en6328787302023-05-08 16:08:462023-05-08 16:09:061222AO02testAO03NoneAO04NoneAO02None
\n", "
" ], "text/plain": [ " id submitdate lastpage startlanguage seed \\\n", "0 1 2023-05-08 15:13:33 1 en 1428982518 \n", "1 2 2023-05-08 16:08:20 1 en 852861659 \n", "2 3 2023-05-08 16:09:06 1 en 632878730 \n", "\n", " startdate datestamp Q00 Q01 MainQ01 \\\n", "0 2023-05-08 15:02:53 2023-05-08 15:13:33 1 this is a test AO01 \n", "1 2023-05-08 16:08:09 2023-05-08 16:08:20 2 222 AO03 \n", "2 2023-05-08 16:08:46 2023-05-08 16:09:06 1 222 AO02 \n", "\n", " MainQ01[comment] MainQ02 MainQ02[comment] ExtraQ01 ExtraQ01[comment] \\\n", "0 None AO02 testing notes AO03 None \n", "1 None None None None None \n", "2 test AO03 None AO04 None \n", "\n", " ExtraQ02 ExtraQ02[comment] \n", "0 AO01 None \n", "1 None None \n", "2 AO02 None " ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "url = \"https://lgs-car.limesurvey.net/admin/remotecontrol\"\n", "get_responses(\n", " online=dict(\n", " base_url=url,\n", " user_id=1,\n", " sid=224783\n", " )\n", ")" ] }, { "cell_type": "markdown", "metadata": { "collapsed": false }, "source": [ "### Support legacy xls or xlsx logbook file\n", "Prior to LimeSurvey, the maintenance notes and marks are collected within a spreadsheet file. The following functions add legacy support, transfromig the xls notation into a pandas Dataframe ready to be parsed with" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [], "source": [ "#|export\n", "def read_logbook(lfile):\n", " '''\n", " Load logbook file and store it as dictionary of rec arrays with stID keys.\n", "\n", " Parameters\n", " ----------\n", " cfile: string\n", " path and filename of loogbook file -> should be .xls file\n", " each sheet represent a maintenance periode.\n", " First row of .xls file have to include column names\n", " *box*, *date*, *clean*, *clean_tilt*, *level*, *level_tilt*, *Hangle*, *Vangle*, *notes*\n", "\n", " Returns\n", " -------\n", " logbook: dict\n", " dict of recarray for each station ID including quality flags from each maintenance cicle.\n", " '''\n", " dtype_log =[\n", " ('box', np.uint8),\n", " ('site', 'U50'),\n", " ('serial_pyr', 'U50'),\n", " ('serial_pyr_tilt', 'U50'),\n", " ('user', 'U50'),\n", " ('campaign', 'U50'),\n", " ('date', 'datetime64[ms]' ),\n", " ('clean', np.uint8),\n", " ('clean_tilt', np.uint8),\n", " ('level', np.uint8),\n", " ('level_tilt', np.uint8),\n", " ('Hangle', 'f8'),\n", " ('Vangle', 'f8'),\n", " ('notes', 'U50')\n", " ]\n", " def _parse_name(name):\n", " key=name.lower().strip()\n", " if key in ['date']:\n", " return 'date'\n", " elif key in ['clean','cleanliness','clean(pyr1)','clean1']:\n", " return 'clean'\n", " elif key in ['clean_tilt','clean2','clean(pyr2)']:\n", " return 'clean_tilt'\n", " elif key in ['level','level(pyr1)','level1']:\n", " return 'level'\n", " elif key in ['level_tilt','level(pyr2)','level2']:\n", " return 'level_tilt'\n", " elif key in ['box','station','id','pyrbox','pyranometerbox']:\n", " return 'box'\n", " elif key in ['hangle','azimuth','azi','horizontal_angle']:\n", " return 'Hangle'\n", " elif key in ['vangle','zenith','zen','vertical_angle']:\n", " return 'Vangle'\n", " elif key in ['notes','note','description']:\n", " return 'notes'\n", " elif key in ['serial','serial1','serial_pyr','pyranometerID','pyrID']:\n", " return 'serial_pyr'\n", " elif key in ['serial2','serial_pyr2','serial_pyr_tilt']:\n", " return 'serial_pyr_tilt'\n", " elif key in ['site','location']:\n", " return 'site'\n", " elif key in ['user','author']:\n", " return 'user'\n", " elif key in ['campaign']:\n", " return 'campaign'\n", " elif key == 'index':\n", " return False\n", " else:\n", " return False\n", " def _hstack2(arrays):\n", " return arrays[0].__array_wrap__(np.hstack(arrays))\n", "\n", " logbook={}\n", " df=pd.read_excel(lfile,sheet_name=None)#,engine='openpyxl')\n", " for sheet in df.keys():# read all sheets from xls file\n", " sh = df[sheet].dropna(axis=0,how='all',subset=['date']) #remove empty lines\n", " sh = sh.dropna(axis=1,how='all') # remove empty columns\n", " for row in sh.itertuples(index=True,name='Pandas'):\n", " A=np.zeros(1,dtype=dtype_log).view(np.recarray)\n", " for name,value in row._asdict().items():\n", " key=_parse_name(name)\n", " if key:\n", " if dict(dtype_log)[key]==np.uint8 and np.isnan(value):\n", " value=9\n", " A[key]=value\n", " if str(A.box[0]) in logbook.keys():\n", " logbook.update({str(A.box[0]):_hstack2([logbook[str(A.box[0])],A])})\n", " else:\n", " logbook.update({str(A.box[0]):A})\n", " return logbook" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [], "source": [ "#|export\n", "def parse_legacy_logbook(fn):\n", " df = None\n", " lb = read_logbook(fn)\n", " for box in lb:\n", " lbb = lb[box]\n", " N = lbb['date'].shape[0]\n", " faccept = [1,2,3,4]\n", " dfb = pd.DataFrame(\n", " {\"datestamp\": lbb['date'],\n", " \"Q00\": box,\n", " \"Q01\": lbb['notes'],\n", " \"MainQ01[comment]\": np.repeat(\"\",N),\n", " \"MainQ02[comment]\": np.repeat(\"\",N),\n", " \"ExtraQ01[comment]\": np.repeat(\"\",N),\n", " \"ExtraQ02[comment]\": np.repeat(\"\",N),\n", " \"MainQ01\": [f\"AO0{f}\" if f in faccept else \"None\" for f in lbb['clean']],\n", " \"MainQ02\": [f\"AO0{f}\" if f in faccept else \"None\" for f in lbb['level']],\n", " \"ExtraQ01\": [f\"AO0{f}\" if f in faccept else \"None\" for f in lbb['clean_tilt']],\n", " \"ExtraQ02\": [f\"AO0{f}\" if f in faccept else \"None\" for f in lbb['level_tilt']],\n", " }\n", " )\n", " if df is None:\n", " df = dfb.copy()\n", " else:\n", " df = pd.concat((df,dfb),ignore_index=True)\n", " df = df.fillna(\"None\")\n", " return df" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "pycharm": { "is_executing": true }, "tags": [ "hide-output" ] }, "outputs": [], "source": [ "#|dropout\n", "fn_lb = \"../../example_data/legacy_logbook.xls\"\n", "\n", "lb = read_logbook(fn_lb)\n", "lb" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "pycharm": { "is_executing": true }, "tags": [ "hide-output" ] }, "outputs": [], "source": [ "#|dropout\n", "parse_legacy_logbook(fn_lb)" ] }, { "cell_type": "markdown", "metadata": { "collapsed": false }, "source": [ "## Parse Responses to dict\n", "Maintenance Flags shall be parsed to a dictionary sorted by the PyrNet box number. The Survey reports are collected over the entire campaign. Therefore, consider only reports within a certain time interval around maintenance time (2 days) for quality flagging the measurement periode.\n", "\n", "Reports within +-2 days around maintenance time are considered, giving the opportunity for corrections within this time frame by issuing another response (or via insert in the website interface). For example, the first report at 1PM includes the quality marks and some notes. Later, if one want to add notes for this station or correct marks, another report can be filled. The Values will be updated by the parsing function:\n", " * Valid Marks (not None) of the latest report within +-2days\n", " * Notes of multiple reports are attached (separated by \";\") starting with the oldest report notes." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "pycharm": { "is_executing": true } }, "outputs": [], "source": [ "#|export\n", "_pollution_marks = {\n", " \"None\":4,\n", " \"AO01\":0,\n", " \"AO02\":1,\n", " \"AO03\":2,\n", " \"AO04\":3,\n", "}\n", "_alignment_marks = {\n", " \"None\":4,\n", " \"AO01\":0,\n", " \"AO02\":1,\n", " \"AO03\":2,\n", "}\n", "_note_keys = {\n", " \"note_general\": \"Q01\",\n", " \"note_align\": \"MainQ01[comment]\",\n", " \"note_clean\": \"MainQ02[comment]\",\n", " \"note_align2\": \"ExtraQ01[comment]\",\n", " \"note_clean2\": \"ExtraQ02[comment]\",\n", "}\n", "_mark_keys = {\n", " \"clean\": \"MainQ01\",\n", " \"align\": \"MainQ02\",\n", " \"clean2\": \"ExtraQ01\",\n", " \"align2\": \"ExtraQ02\",\n", "}\n", "\n", "def parse_report(\n", " df: pd.DataFrame,\n", " date_of_maintenance: float | dt.datetime | np.datetime64 | None,\n", ") -> dict:\n", " \"\"\"\n", " Use pandas.read_csv (sep=;) to parse the survey report.\n", "\n", " Parameters\n", " ----------\n", " df: Dataframe\n", " LimeSurvey response parsed as pandas Dataframe.\n", " date_of_maintenance: float, datetime, datetime64 or None\n", " A rough date of maintenance (at least day resolution).\n", " If float, interpreted as Julian day from 2000-01-01T12:00.\n", " If None, the most recent logbook entries will be parsed.\n", "\n", " Returns\n", " -------\n", " dict\n", " Dictionary storing maintenance flags and notes by PyrNet box number.\n", " \"\"\"\n", " if date_of_maintenance is not None:\n", " date_of_maintenance = utils.to_datetime64(date_of_maintenance)\n", "\n", " results = {}\n", " for i in range(df.shape[0]):\n", " box = int(df[\"Q00\"].values[i])\n", " key = f\"{box:03d}\"\n", "\n", " # consider only reports +-2 days around date of maintenance\n", " mdate = pd.to_datetime(df['datestamp'][i])\n", " if date_of_maintenance is None:\n", " dtime = np.abs(mdate - np.max(df['datestamp']))\n", " else:\n", " dtime = np.abs(mdate - date_of_maintenance)\n", "\n", " if dtime > np.timedelta64(2,'D'):\n", " continue\n", "\n", " # store report in dictionary\n", "\n", " if key not in results:\n", " # initialize marks\n", " for mkey in _mark_keys:\n", " results = assoc_in(results, [key,mkey], 4)\n", " # initialize notes\n", " for nkey in _note_keys:\n", " results = assoc_in(results, [key,nkey], \"\")\n", "\n", " # merge notes if multiple reports exist\n", " for nkey in _note_keys:\n", " new_note = df[_note_keys[nkey]].values[i]\n", " update_note = (results[key][nkey]+'; '+new_note).strip('; ')\n", " results = assoc_in(results, [key,nkey], update_note)\n", "\n", " # update marks with most recent report if not None\n", " for mkey in _mark_keys:\n", " new_mark = df[_mark_keys[mkey]][i]\n", " if new_mark==\"None\":\n", " continue\n", " if mkey.startswith(\"clean\"):\n", " new_mark = _pollution_marks[new_mark]\n", " else:\n", " new_mark = _alignment_marks[new_mark]\n", " results = assoc_in(results, [key,mkey], new_mark)\n", " return results\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "pycharm": { "is_executing": true }, "tags": [ "hide-output" ] }, "outputs": [], "source": [ "#|dropout\n", "# Parse LimeSurvey report\n", "fn = \"../../example_data/results-survey224783.csv\"\n", "\n", "df = pd.read_csv(fn, sep=';')\n", "df = df.fillna(\"None\")\n", "\n", "parse_report(df, np.datetime64(\"2023-05-08T12:00\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "pycharm": { "is_executing": true }, "tags": [ "hide-output" ] }, "outputs": [], "source": [ "#|dropout\n", "fn_lb = \"../../example_data/legacy_logbook.xls\"\n", "# Parse legacy xls notebook\n", "parse_report(parse_legacy_logbook(fn_lb), np.datetime64(\"2019-06-17T12:00\"))" ] }, { "cell_type": "markdown", "metadata": { "collapsed": false }, "source": [ "## Make aggregated quality flags\n", "Aggregate quality marks to a binary number according to CF-Convention section 3.5 for quality flags.\n", "\n", "QC flag binary representation, bits: XXYY with:\n", "* XX - level - [00,01,10] - good, slight out of level, bad out of level\n", "* YY - clean - [00,01,10,11] good, slight-, moderate-, strong covered\n", "```\n", "flag_mask = '3b,3b,3b, 12b, 12b'\n", "flag_values = '1b, 2b, 3b, 4b, 8b '\n", "flag_meanings = \"\n", " soiling_light\n", " soiling_moderate\n", " soiling_heavy\n", " level_problematic\n", " level_bad\"\n", "```\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "pycharm": { "is_executing": true } }, "outputs": [], "source": [ "#|export\n", "def get_qcflag(qc_clean, qc_level):\n", " \"\"\"\n", " Aggregate quality flags.\n", "\n", " Parameters\n", " ----------\n", " qc_clean: int\n", " [0,1,2,3] [clean, slight-, moderate-, strong covered]\n", " qc_level: int\n", " [0,1,2] [good, slight misalignment, strong misalignment]\n", "\n", " Returns\n", " -------\n", " int\n", " aggregated quality flagg [0-11]\n", " \"\"\"\n", " qc = (qc_level<<2) + qc_clean\n", " return qc\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "pycharm": { "is_executing": true } }, "outputs": [], "source": [ "get_qcflag(np.array([0,1,2,3]),np.array([0,1,2,2]))\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "pycharm": { "is_executing": true }, "tags": [ "remove-cell", "hide-input", "hide-output" ] }, "outputs": [], "source": [ "#|hide\n", "# Export module\n", "# Requires *nbdev* to export and update the *reports.py* module\n", "import nbdev.export\n", "import nbformat as nbf\n", "name = \"reports\"\n", "\n", "# Export python module\n", "nbdev.export.nb_export( f\"{name}.ipynb\" ,f\"../../src/pyrnet\")\n", "\n", "# Export to docs\n", "ntbk = nbf.read(f\"{name}.ipynb\", nbf.NO_CONVERT)\n", "\n", "text_search_dict = {\n", " \"#|hide\": \"remove-cell\", # Remove the whole cell\n", " \"#|dropcode\": \"hide-input\", # Hide the input w/ a button to show\n", " \"#|dropout\": \"hide-output\" # Hide the output w/ a button to show\n", "}\n", "for cell in ntbk.cells:\n", " cell_tags = cell.get('metadata', {}).get('tags', [])\n", " for key, val in text_search_dict.items():\n", " if key in cell['source']:\n", " if val not in cell_tags:\n", " cell_tags.append(val)\n", " if len(cell_tags) > 0:\n", " cell['metadata']['tags'] = cell_tags\n", " nbf.write(ntbk, f\"../../docs/source/nbs/{name}.ipynb\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" } }, "nbformat": 4, "nbformat_minor": 0 }