From e69184259a1b91e1b8683bb160ccb431e3ab9c84 Mon Sep 17 00:00:00 2001 From: HarshCasper Date: Wed, 2 Mar 2022 12:02:18 +0530 Subject: [PATCH 1/4] CI: Implement a publish action to push Docker images Signed-off-by: HarshCasper --- .github/workflows/ci.yml | 2 -- .github/workflows/publish.yml | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1fe980..a81bd27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,6 @@ name: Continuous Integration on: - push: - branches: [ main, master ] pull_request: branches: [ main, master ] workflow_dispatch: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..0782d4d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,51 @@ +name: Taswira CD + +on: + schedule: + - cron: "0 0 * * MON" + + push: + branches: [ master ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + +env: + REGISTRY: ghcr.io + +jobs: + build: + name: Build and publish Taswira + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Log into GitHub Container Registry + uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/moja-global/taswira + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From f86bb188692c340813232d1528971647c8b1757a Mon Sep 17 00:00:00 2001 From: HarshCasper Date: Wed, 2 Mar 2022 17:06:31 +0530 Subject: [PATCH 2/4] CHORE: Format all source files using Black Signed-off-by: HarshCasper --- src/taswira/app.py | 313 +++++++++++++++++-------------- src/taswira/scripts/arg_types.py | 3 +- src/taswira/scripts/console.py | 56 +++--- src/taswira/scripts/ingestion.py | 44 +++-- src/taswira/scripts/metadata.py | 18 +- src/taswira/units.py | 1 + 6 files changed, 241 insertions(+), 194 deletions(-) diff --git a/src/taswira/app.py b/src/taswira/app.py index e121d99..bca814f 100644 --- a/src/taswira/app.py +++ b/src/taswira/app.py @@ -9,8 +9,10 @@ from dash.dependencies import Input, Output, State from terracotta.handlers.colormap import colormap as get_colormap -BASE_MAP_ATTRIBUTION = ('© ' - 'OpenStreetMap contributors') +BASE_MAP_ATTRIBUTION = ( + '© ' + "OpenStreetMap contributors" +) N_COLORBAR_ROWS = 6 @@ -60,16 +62,18 @@ def get_colorbar(stretch_range, colormap): """ ctg = [ f'{cmap["value"]:.3f}+' - for cmap in get_colormap(stretch_range=stretch_range, - colormap=colormap, - num_values=N_COLORBAR_ROWS) + for cmap in get_colormap( + stretch_range=stretch_range, colormap=colormap, num_values=N_COLORBAR_ROWS + ) ] - return dlx.categorical_colorbar(categories=ctg, - colorscale=colormap, - width=20, - height=100, - position="bottomright") + return dlx.categorical_colorbar( + categories=ctg, + colorscale=colormap, + width=20, + height=100, + position="bottomright", + ) def get_app(): @@ -84,81 +88,91 @@ def get_app(): # pylint: disable=unused-variable data = _get_data() app = dash.Dash(__name__, server=False) - app.title = 'Taswira' - options = [{'label': k, 'value': k} for k in list(data)] + app.title = "Taswira" + options = [{"label": k, "value": k} for k in list(data)] app.layout = html.Div( [ - dcc.Store(id='raster-layers-store'), - dcc.Dropdown(id='title-dropdown', - clearable=False, - options=options, - value=options[0]['value'], - style={ - 'position': 'relative', - 'top': '5px', - 'zIndex': '500', - 'height': '0', - 'maxWidth': '200px', - 'marginLeft': 'auto', - 'marginRight': '10px' - }), - html.Div(dl.Map([ - dl.TileLayer(attribution=BASE_MAP_ATTRIBUTION), - dl.LayerGroup(id='raster-layers'), - dl.LayerGroup(id='colorbar-layer') - ], - id='main-map'), - id='main-map-div', - style={ - 'position': 'relative', - 'width': '100%', - 'height': '70%', - 'top': '0', - 'left': '0' - }), + dcc.Store(id="raster-layers-store"), + dcc.Dropdown( + id="title-dropdown", + clearable=False, + options=options, + value=options[0]["value"], + style={ + "position": "relative", + "top": "5px", + "zIndex": "500", + "height": "0", + "maxWidth": "200px", + "marginLeft": "auto", + "marginRight": "10px", + }, + ), + html.Div( + dl.Map( + [ + dl.TileLayer(attribution=BASE_MAP_ATTRIBUTION), + dl.LayerGroup(id="raster-layers"), + dl.LayerGroup(id="colorbar-layer"), + ], + id="main-map", + ), + id="main-map-div", + style={ + "position": "relative", + "width": "100%", + "height": "70%", + "top": "0", + "left": "0", + }, + ), html.Div( [ - html.Button(id='animation-btn'), - dcc.Interval(id='animation-interval', disabled=True) + html.Button(id="animation-btn"), + dcc.Interval(id="animation-interval", disabled=True), ], style={ - 'position': 'relative', - 'top': '-50px', - 'left': '10px', - 'zIndex': '500', - 'height': '0', + "position": "relative", + "top": "-50px", + "left": "10px", + "zIndex": "500", + "height": "0", }, - id="animation-control"), + id="animation-control", + ), html.Div( - [dcc.Slider( - id='year-slider', - step=None, - value=0, - )], + [ + dcc.Slider( + id="year-slider", + step=None, + value=0, + ) + ], style={ - 'position': 'relative', - 'top': '-50px', - 'left': '60px', - 'zIndex': '500', - 'height': '0', - 'marginRight': '9em' + "position": "relative", + "top": "-50px", + "left": "60px", + "zIndex": "500", + "height": "0", + "marginRight": "9em", }, - id='year-slider-div'), - dcc.Graph(id='indicator-change-graph', - responsive=True, - style={ - 'width': '100%', - 'height': '30%' - }) + id="year-slider-div", + ), + dcc.Graph( + id="indicator-change-graph", + responsive=True, + style={"width": "100%", "height": "30%"}, + ), ], style={ - 'position': 'absolute', - 'width': '100%', - 'height': '100%', - 'top': '0', - 'left': '0', - 'fontFamily': 'sans-serif' - }) + "position": "absolute", + "width": "100%", + "height": "100%", + "top": "0", + "left": "0", + "fontFamily": "sans-serif", + }, + ) app.clientside_callback( """ @@ -170,68 +184,75 @@ def get_app(): return l; }); } - """, Output('raster-layers', 'children'), - [Input('year-slider', 'value'), - Input('raster-layers-store', 'data')], - [State('raster-layers', 'children')]) - - @app.callback([ - Output('raster-layers-store', 'data'), - Output('colorbar-layer', 'children'), - Output('main-map', 'bounds') - ], [Input('title-dropdown', 'value')]) + """, + Output("raster-layers", "children"), + [Input("year-slider", "value"), Input("raster-layers-store", "data")], + [State("raster-layers", "children")], + ) + + @app.callback( + [ + Output("raster-layers-store", "data"), + Output("colorbar-layer", "children"), + Output("main-map", "bounds"), + ], + [Input("title-dropdown", "value")], + ) def update_raster_layers_colobar_map_bounds(title): - ranges = [data[title][year]['range'] for year in data[title].keys()] + ranges = [data[title][year]["range"] for year in data[title].keys()] lowers, uppers = list(zip(*ranges)) stretch_range = [min(lowers), max(uppers)] - xyz = '{z}/{x}/{y}' + xyz = "{z}/{x}/{y}" layers = [] for year in data[title]: raster_data = data[title][year] - colormap = raster_data['metadata']['colormap'] - bounds = format_bounds(raster_data['bounds']) + colormap = raster_data["metadata"]["colormap"] + bounds = format_bounds(raster_data["bounds"]) layers.append( dl.TileLayer( - url= - f'/singleband/{title}/{year}/{xyz}.png?colormap={colormap}', + url=f"/singleband/{title}/{year}/{xyz}.png?colormap={colormap}", opacity=0, - id=year)) + id=year, + ) + ) colorbar = get_colorbar(stretch_range, colormap) return layers, [colorbar], bounds - @app.callback([ - Output('year-slider', 'marks'), - Output('year-slider', 'min'), - Output('year-slider', 'max'), - ], [Input('title-dropdown', 'value')]) + @app.callback( + [ + Output("year-slider", "marks"), + Output("year-slider", "min"), + Output("year-slider", "max"), + ], + [Input("title-dropdown", "value")], + ) def update_slider(title): - mark_style = {'color': '#fff', 'textShadow': '1px 1px 2px #000'} - marks = { - int(k): dict(label=k, style=mark_style) - for k in data[title].keys() - } + mark_style = {"color": "#fff", "textShadow": "1px 1px 2px #000"} + marks = {int(k): dict(label=k, style=mark_style) for k in data[title].keys()} min_value = min(marks.keys()) max_value = max(marks.keys()) return marks, min_value, max_value - @app.callback(Output('year-slider', 'value'), [ - Input('year-slider', 'marks'), - Input('animation-interval', 'n_intervals') - ], [State('year-slider', 'value')]) - def update_slider_value(marks, n_intervals, current_value): # pylint: disable=unused-argument + @app.callback( + Output("year-slider", "value"), + [Input("year-slider", "marks"), Input("animation-interval", "n_intervals")], + [State("year-slider", "value")], + ) + def update_slider_value( + marks, n_intervals, current_value + ): # pylint: disable=unused-argument ctx = dash.callback_context min_value = min(marks.keys()) if ctx.triggered: - trigger = ctx.triggered[0]['prop_id'].split('.')[0] - trigger_value = ctx.triggered[0]['value'] - if trigger == 'animation-interval' and trigger_value: - new_value = get_element_after(str(current_value), - iter(marks.keys())) + trigger = ctx.triggered[0]["prop_id"].split(".")[0] + trigger_value = ctx.triggered[0]["value"] + if trigger == "animation-interval" and trigger_value: + new_value = get_element_after(str(current_value), iter(marks.keys())) if new_value is not None: return int(new_value) elif current_value: @@ -239,49 +260,59 @@ def update_slider_value(marks, n_intervals, current_value): # pylint: disable=u return int(min_value) - @app.callback(Output('indicator-change-graph', 'figure'), - [Input('title-dropdown', 'value')]) + @app.callback( + Output("indicator-change-graph", "figure"), [Input("title-dropdown", "value")] + ) def update_graph(title): fig = go.Figure() x_marks = [] y_margs = [] for year, meta in data[title].items(): x_marks.append(year) - y_margs.append(meta['metadata']['indicator_value']) - fig.add_trace(go.Scatter(x=x_marks, y=y_margs, mode='lines+markers')) + y_margs.append(meta["metadata"]["indicator_value"]) + fig.add_trace(go.Scatter(x=x_marks, y=y_margs, mode="lines+markers")) - unit = '' + unit = "" for _, meta in data[title].items(): - unit = meta['metadata']['unit'] + unit = meta["metadata"]["unit"] break - fig.update_layout(autosize=False, - xaxis_title='Year', - yaxis_title=f'{title} ({unit})', - xaxis_type='category', - height=150, - margin=dict(t=10, b=0)) + fig.update_layout( + autosize=False, + xaxis_title="Year", + yaxis_title=f"{title} ({unit})", + xaxis_type="category", + height=150, + margin=dict(t=10, b=0), + ) return fig - @app.callback(Output('animation-control', 'children'), - [Input('animation-btn', 'n_clicks')], [ - State('animation-btn', 'value'), - ]) - def update_animation_control(n_clicks, current_value): # pylint: disable=unused-argument - new_value = 'pause' if current_value == 'play' else 'play' - btn = html.Button(new_value.capitalize(), - value=new_value, - id='animation-btn', - style={ - 'height': '30px', - 'backgroundColor': '#fff', - 'textAlign': 'center', - 'borderRadius': '4px', - 'border': '2px solid rgba(0,0,0,0.2)', - 'fontWeight': 'bold' - }) - is_paused = (new_value == 'play') - interval = dcc.Interval(id='animation-interval', disabled=is_paused) + @app.callback( + Output("animation-control", "children"), + [Input("animation-btn", "n_clicks")], + [ + State("animation-btn", "value"), + ], + ) + def update_animation_control( + n_clicks, current_value + ): # pylint: disable=unused-argument + new_value = "pause" if current_value == "play" else "play" + btn = html.Button( + new_value.capitalize(), + value=new_value, + id="animation-btn", + style={ + "height": "30px", + "backgroundColor": "#fff", + "textAlign": "center", + "borderRadius": "4px", + "border": "2px solid rgba(0,0,0,0.2)", + "fontWeight": "bold", + }, + ) + is_paused = new_value == "play" + interval = dcc.Interval(id="animation-interval", disabled=is_paused) return [btn, interval] return app diff --git a/src/taswira/scripts/arg_types.py b/src/taswira/scripts/arg_types.py index 80b7ae3..4b86833 100644 --- a/src/taswira/scripts/arg_types.py +++ b/src/taswira/scripts/arg_types.py @@ -39,7 +39,8 @@ def indicator_file(path): for key in INDICATOR_REQUIRED_KEYS: if not key in indicator: raise argparse.ArgumentTypeError( - f"Required key `{key}` missing in config element {i}.") + f"Required key `{key}` missing in config element {i}." + ) return config diff --git a/src/taswira/scripts/console.py b/src/taswira/scripts/console.py index cbb0763..adbe208 100644 --- a/src/taswira/scripts/console.py +++ b/src/taswira/scripts/console.py @@ -25,45 +25,53 @@ def start_servers(dbpath, port): dbpath: Path to a Terracota-generated DB. port: Port number for Terracotta server. """ + def handler(signum, frame): # pylint: disable=unused-argument sys.exit(0) signal.signal(signal.SIGINT, handler) - tc.update_settings(DRIVER_PATH=dbpath, DRIVER_PROVIDER='sqlite') + tc.update_settings(DRIVER_PATH=dbpath, DRIVER_PROVIDER="sqlite") app = get_app() app.init_app(tc_app) def open_browser(): - webbrowser.open(f'http://localhost:{port}') + webbrowser.open(f"http://localhost:{port}") threading.Timer(2, open_browser).start() - if 'DEBUG' in os.environ: + if "DEBUG" in os.environ: app.run_server(port=port, threaded=False, debug=True) else: - print('Starting Taswira...') - run_simple('localhost', port, app.server) + print("Starting Taswira...") + run_simple("localhost", port, app.server) def console(): """The command-line interface for Taswira""" parser = argparse.ArgumentParser( - description="Interactive visualization tool for GCBM") + description="Interactive visualization tool for GCBM" + ) parser.add_argument( "config", type=arg_types.indicator_file, help="path to JSON config file", ) - parser.add_argument("spatial_results", - type=arg_types.spatial_results, - help="path to GCBM spatial output directory") - parser.add_argument("db_results", - type=arg_types.db_results, - help="path to compiled GCBM results database") - parser.add_argument("--allow-unoptimized", - action="store_true", - help="allow processing unoptimized raster files") + parser.add_argument( + "spatial_results", + type=arg_types.spatial_results, + help="path to GCBM spatial output directory", + ) + parser.add_argument( + "db_results", + type=arg_types.db_results, + help="path to compiled GCBM results database", + ) + parser.add_argument( + "--allow-unoptimized", + action="store_true", + help="allow processing unoptimized raster files", + ) args = parser.parse_args() update_config(args.config) @@ -71,14 +79,19 @@ def console(): with tempfile.TemporaryDirectory() as tmpdirname: try: if args.allow_unoptimized: - warnings.simplefilter('ignore') # Supress Terracotta warnings - - dbpath = ingest(args.spatial_results, args.db_results, tmpdirname, - args.allow_unoptimized) + warnings.simplefilter("ignore") # Supress Terracotta warnings + + dbpath = ingest( + args.spatial_results, + args.db_results, + tmpdirname, + args.allow_unoptimized, + ) port = get_free_port() start_servers(dbpath, port) except UnoptimizedRaster: - sys.exit("""\ + sys.exit( + """\ Found a raster file that is not a valid cloud-optimized GeoTIFFs. This tool wasn't designed to work with such files. You can try continuing anyway by passing the `--allow-unoptimized` flag but it's not recommended. @@ -87,6 +100,7 @@ def console(): the following GDAL parameters: BIGTIFF=YES, TILED=YES, COMPRESS=ZSTD, ZSTD_LEVEL=1 -""") +""" + ) except KeyboardInterrupt: sys.exit("Raster loading was interrupted") diff --git a/src/taswira/scripts/ingestion.py b/src/taswira/scripts/ingestion.py index c853e64..f40a831 100644 --- a/src/taswira/scripts/ingestion.py +++ b/src/taswira/scripts/ingestion.py @@ -11,12 +11,12 @@ from . import get_config from .metadata import get_metadata -DB_NAME = 'terracotta.sqlite' -GCBM_RASTER_NAME_PATTERN = r'.*_(?P\d{4}).tiff' -GCBM_RASTER_KEYS = ('title', 'year') +DB_NAME = "terracotta.sqlite" +GCBM_RASTER_NAME_PATTERN = r".*_(?P\d{4}).tiff" +GCBM_RASTER_KEYS = ("title", "year") GCBM_RASTER_KEYS_DESCRIPTION = { - 'title': 'Name of indicator', - 'year': 'Year of raster data', + "title": "Name of indicator", + "year": "Year of raster data", } @@ -24,10 +24,9 @@ def _find_raster_year(raster_path): raster_filename = os.path.basename(raster_path) match = re.match(GCBM_RASTER_NAME_PATTERN, raster_filename) if match is None: - raise ValueError( - f'Input file {raster_filename} does not match raster pattern') + raise ValueError(f"Input file {raster_filename} does not match raster pattern") - return match.group('year') + return match.group("year") class UnoptimizedRaster(Exception): @@ -46,33 +45,32 @@ def ingest(rasterdir, db_results, outputdir, allow_unoptimized=False): Returns: Path to generated DB. """ - driver = get_driver(os.path.join(outputdir, DB_NAME), provider='sqlite') + driver = get_driver(os.path.join(outputdir, DB_NAME), provider="sqlite") driver.create(GCBM_RASTER_KEYS, GCBM_RASTER_KEYS_DESCRIPTION) - progress = tqdm.tqdm(get_config(), desc='Searching raster files') + progress = tqdm.tqdm(get_config(), desc="Searching raster files") raster_files = [] for config in progress: - for file in glob.glob(rasterdir + os.sep + config['file_pattern']): + for file in glob.glob(rasterdir + os.sep + config["file_pattern"]): if not is_valid_cog(file) and not allow_unoptimized: raise UnoptimizedRaster raster_files.append(dict(path=file, **config)) with driver.connect(): metadata = get_metadata(db_results) - progress = tqdm.tqdm(raster_files, desc='Processing raster files') + progress = tqdm.tqdm(raster_files, desc="Processing raster files") for raster in progress: - title = raster.get('title', raster['database_indicator']) - year = _find_raster_year(raster['path']) - unit = find_units(raster.get('graph_units')) + title = raster.get("title", raster["database_indicator"]) + year = _find_raster_year(raster["path"]) + unit = find_units(raster.get("graph_units")) computed_metadata = driver.compute_metadata( - raster['path'], + raster["path"], extra_metadata={ - 'indicator_value': str(metadata[title][year]), - 'colormap': raster.get('palette').lower(), - 'unit': unit.value[2] - }) - driver.insert((title, year), - raster['path'], - metadata=computed_metadata) + "indicator_value": str(metadata[title][year]), + "colormap": raster.get("palette").lower(), + "unit": unit.value[2], + }, + ) + driver.insert((title, year), raster["path"], metadata=computed_metadata) return driver.path diff --git a/src/taswira/scripts/metadata.py b/src/taswira/scripts/metadata.py index b8e0d12..455eeaf 100644 --- a/src/taswira/scripts/metadata.py +++ b/src/taswira/scripts/metadata.py @@ -14,16 +14,16 @@ def _get_simulation_years(conn): - years = conn.execute( - "SELECT MIN(year), MAX(year) from v_age_indicators").fetchone() + years = conn.execute("SELECT MIN(year), MAX(year) from v_age_indicators").fetchone() return years def _find_indicator_table(conn, indicator): for table, value_col in RESULTS_TABLES.items(): - if conn.execute(f"SELECT 1 FROM {table} WHERE indicator = ?", - [indicator]).fetchone(): + if conn.execute( + f"SELECT 1 FROM {table} WHERE indicator = ?", [indicator] + ).fetchone(): return table, value_col return None, None @@ -34,7 +34,8 @@ def _get_annual_result(conn, indicator, units=Units.Tc): _, units_tc, _ = units.value start_year, end_year = _get_simulation_years(conn) - db_result = conn.execute(f""" + db_result = conn.execute( + f""" SELECT years.year, COALESCE(SUM(i.{value_col}), 0) / {units_tc} AS value FROM (SELECT DISTINCT year FROM v_age_indicators ORDER BY year) AS years LEFT JOIN {table} i @@ -43,7 +44,8 @@ def _get_annual_result(conn, indicator, units=Units.Tc): AND (years.year BETWEEN {start_year} AND {end_year}) GROUP BY years.year ORDER BY years.year - """).fetchall() + """ + ).fetchall() data = OrderedDict() for year, value in db_result: @@ -64,7 +66,7 @@ def get_metadata(db_results): metadata = {} conn = sqlite3.connect(db_results) for config in get_config(): - indicator = config['database_indicator'] - title = config.get('title', indicator) + indicator = config["database_indicator"] + title = config.get("title", indicator) metadata[title] = _get_annual_result(conn, indicator) return metadata diff --git a/src/taswira/units.py b/src/taswira/units.py index 1a3f821..f647280 100644 --- a/src/taswira/units.py +++ b/src/taswira/units.py @@ -4,6 +4,7 @@ class Units(Enum): """Enum of units containing tuple (IsPerHa, Mult, Label)""" + Blank = False, 1, "" Tc = False, 1, "tC" Ktc = False, 1e3, "KtC" From cb504ebe931535370b7d7a0c359d88fa9b85dbc9 Mon Sep 17 00:00:00 2001 From: HarshCasper Date: Wed, 2 Mar 2022 17:12:28 +0530 Subject: [PATCH 3/4] CI: Refactoring CI/CD for more efficiency Signed-off-by: HarshCasper --- .github/workflows/ci.yml | 61 ++++++++++++++++--- .github/workflows/{publish.yml => docker.yml} | 9 ++- environment.yml | 2 + 3 files changed, 60 insertions(+), 12 deletions(-) rename .github/workflows/{publish.yml => docker.yml} (86%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a81bd27..89b562c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,22 +1,65 @@ -# This workflow will install Conda environment, run tests and lint with a single version of Python. - name: Continuous Integration on: + schedule: + - cron: "0 0 * * MON" + push: + branches: [ master ] pull_request: - branches: [ main, master ] + branches: [ master ] workflow_dispatch: jobs: ci: name: CI runs-on: ubuntu-latest - env: - DOCKER_BUILDKIT: "1" + steps: - name: Checkout code uses: actions/checkout@v2 - - name: Lint using pylint - run: docker build . --target lint - - name: Test using pytest - run: docker build . --target unit-test + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Setup Conda + uses: conda-incubator/setup-miniconda@v2 + with: + python-version: 3.7 + mamba-version: "*" + channels: conda-forge + channel-priority: true + activate-environment: taswira + + - name: Cache conda + uses: actions/cache@v2 + env: + # Increase this value to reset cache if environment.yml has not changed + CACHE_NUMBER: 0 + with: + path: /usr/local/miniconda/envs/taswira + key: + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }} + id: envcache + + - name: Update Conda Environment + run: mamba env update -n taswira -f environment.yml + + - name: Install Python package + shell: bash -l {0} + run: | + conda activate taswira + python -m pip install -e . + + - name: Run Black + run: | + python -m pip install black + black src --diff + black --check src + + - name: Test + shell: bash -l {0} + run: | + conda activate taswira + python -m pytest \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/docker.yml similarity index 86% rename from .github/workflows/publish.yml rename to .github/workflows/docker.yml index 0782d4d..45c3618 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/docker.yml @@ -1,13 +1,15 @@ -name: Taswira CD +name: Docker CI/CD on: schedule: - cron: "0 0 * * MON" - + pull_request: + branches: [ master ] push: branches: [ master ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] + workflow_dispatch: env: REGISTRY: ghcr.io @@ -28,6 +30,7 @@ jobs: uses: docker/setup-buildx-action@v1 - name: Log into GitHub Container Registry + if: ${{ github.event_name != 'pull_request' }} uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c with: registry: ${{ env.REGISTRY }} @@ -44,7 +47,7 @@ jobs: uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc with: context: . - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/environment.yml b/environment.yml index 92a8ec2..1affecc 100644 --- a/environment.yml +++ b/environment.yml @@ -15,3 +15,5 @@ dependencies: - pylint>=2.5.2 - dash==1.13.3 - dash-leaflet==0.0.19 + - markupsafe<2.1 + - jinja2 From 728a57c59f3cdaa613ec97855aa66eb2d71cf69c Mon Sep 17 00:00:00 2001 From: HarshCasper Date: Fri, 11 Mar 2022 08:56:51 +0530 Subject: [PATCH 4/4] CI: Add comments to workflows as per @aornugent comments Signed-off-by: HarshCasper --- .github/workflows/ci.yml | 10 +++++++--- .github/workflows/docker.yml | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89b562c..4c4b4b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: Continuous Integration on: schedule: + # Run this workflow at 00:00 on every Monday - cron: "0 0 * * MON" push: branches: [ master ] @@ -10,8 +11,8 @@ on: workflow_dispatch: jobs: - ci: - name: CI + build-test-taswira: + name: Build and Test Taswira runs-on: ubuntu-latest steps: @@ -38,12 +39,14 @@ jobs: # Increase this value to reset cache if environment.yml has not changed CACHE_NUMBER: 0 with: + # Cache the path where dependencies are installed with a key unique to the existing environment.yml path: /usr/local/miniconda/envs/taswira key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }} id: envcache - name: Update Conda Environment + # Setup (or update) the environment for faster installs run: mamba env update -n taswira -f environment.yml - name: Install Python package @@ -60,6 +63,7 @@ jobs: - name: Test shell: bash -l {0} + # Test the package run: | conda activate taswira - python -m pytest \ No newline at end of file + python -m pytest diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 45c3618..bb5e9ca 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -2,6 +2,7 @@ name: Docker CI/CD on: schedule: + # Run this workflow at 00:00 on every Monday - cron: "0 0 * * MON" pull_request: branches: [ master ] @@ -40,6 +41,7 @@ jobs: - name: Extract Docker metadata id: meta uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + # It will push the image to ghcr.io/moja-global/taswira:master on push with: images: ${{ env.REGISTRY }}/moja-global/taswira @@ -47,8 +49,10 @@ jobs: uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc with: context: . + # Prevent push to GHCR on pull requests push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + # Implement Docker build caching for faster builds cache-from: type=gha cache-to: type=gha,mode=max