Repository: CS-SI/eodag-labextension (language: python)

Issue:
Add GitHub CI
In GitLab by @ctriquet-cs on Sep 7, 2021, 16:14

A initial process exists into https://gitlab.cloud-espace.si.c-s.fr/vre/ai4geo/lot4/eodag-labextension/-/blob/master/.github/workflows/build.yml.

See eodag example at https://github.com/CS-SI/eodag/tree/develop/.github

Steps are:

- build
- deploy on pypi

See @sbrunato for permissions to access to https://github.com/CS-SI/eodag-labextension and work on a fork and on https://test.pypi.org/ for deployment.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 21d5c6e..3cd7b7b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,11 +2,26 @@ name: Build
 
 on:
   push:
-    branches: main
+    branches: [master, develop]
   pull_request:
-    branches: '*'
+    branches: [master, develop]
+  schedule:
+    - cron: '0 7 * * 1'
+  workflow_dispatch:
 
 jobs:
+  lint:
+    name: Linting (pre-commit)
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout the repo
+        uses: actions/checkout@v2
+      - name: Set up Python 3.6
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.6'
+      - name: Run pre-commit action
+        uses: pre-commit/action@v2.0.0
   build:
     runs-on: ubuntu-latest
     steps:
@@ -19,16 +34,16 @@ jobs:
       - name: Install Python
         uses: actions/setup-python@v2
         with:
-          python-version: '3.7'
+          python-version: '3.6'
           architecture: 'x64'
 
       - name: Setup pip cache
         uses: actions/cache@v2
         with:
           path: ~/.cache/pip
-          key: pip-3.7-${{ hashFiles('package.json') }}
+          key: pip-3.6-${{ hashFiles('package.json') }}
           restore-keys: |
-            pip-3.7-
+            pip-3.6-
             pip-
 
       - name: Get yarn cache directory path
@@ -51,7 +66,7 @@ jobs:
           jlpm run eslint:check
           python -m pip install .
 
-          jupyter server extension list 2>&1 | grep -ie "eodag-labextension.*OK"
+          jupyter server extension list 2>&1 | grep -ie "eodag[_-]labextension.*OK"
 
-          jupyter labextension list 2>&1 | grep -ie "eodag-labextension.*OK"
+          jupyter labextension list 2>&1 | grep -ie "eodag[_-]labextension.*OK"
           python -m jupyterlab.browser_check
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..3bf0960
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,47 @@
+name: Publish to PyPI
+
+on:
+  push:
+    tags:
+      - 'v*'
+  workflow_dispatch:
+
+jobs:
+  build-n-publish:
+    name: Build and publish to PyPI
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout source
+        uses: actions/checkout@v2
+
+      - name: Set up Python 3.7
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.7'
+
+      - name: Check that the current version isn't already on PyPi
+        run: |
+          if [ "$(./get_pypi_latest_version.sh)" != "$(python setup.py --version)" ]
+          then
+            echo "Current version is not on PyPI, proceed with bulding"
+          else
+            echo "Current version is the latest version uploaded to PyPI"
+            exit 1
+          fi
+
+      - name: Check long description is OK for PyPI with tox
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install tox sphinx
+          tox -e pypi
+
+      - name: Build a binary wheel and a source tarball
+        run: |
+          python -m pip install setuptools wheel
+          python setup.py sdist bdist_wheel
+      - name: Publish distribution to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8ce12fb..cf9cf50 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -44,10 +44,15 @@ repos:
       - id: flake8
 
   - repo: https://github.com/pre-commit/mirrors-eslint
-    rev: v7.25.0
+    rev: v7.14.0
     hooks:
       - id: eslint
         additional_dependencies:
+          - eslint@7.14.0
+          - typescript@4.2.4
+          - prettier@2.3.0
+          - '@typescript-eslint/eslint-plugin@4.8.1'
+          - '@typescript-eslint/parser@4.8.1'
           - eslint-config-prettier@^6.15.0
           - eslint-plugin-prettier@^3.1.4
 
diff --git a/package.json b/package.json
index 910fc47..181e26b 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
   "bugs": {
     "url": "https://github.com/CS-SI/eodag-labextension/issues"
   },
-  "license": "Apache v2.0",
+  "license": "Apache-2.0",
   "author": {
     "name": "CS Group",
     "email": "admin@geostorm.eu"
diff --git a/src/Autocomplete.tsx b/src/Autocomplete.tsx
index 4e05b92..f203f7a 100644
--- a/src/Autocomplete.tsx
+++ b/src/Autocomplete.tsx
@@ -18,7 +18,7 @@ function NoOptionsMessage(props: any) {
     <div
       color="textSecondary"
       style={{
-        padding: `8px 16px`
+        padding: '8px 16px'
       }}
       {...props.innerProps}
     >
@@ -86,12 +86,10 @@ interface IProps {
   handleChange: any;
 }
 
-interface IState {}
-
-class IntegrationReactSelect extends React.Component<IProps, IState> {
+class IntegrationReactSelect extends React.Component<IProps> {
   render() {
     const { suggestions, value, handleChange } = this.props;
-    let currentValue: OptionTypeBase = value
+    const currentValue: OptionTypeBase = value
       ? suggestions.find(e => e.value === value)
       : undefined;
     return (
diff --git a/src/BrowseComponent.tsx b/src/BrowseComponent.tsx
index 18133de..313f869 100644
--- a/src/BrowseComponent.tsx
+++ b/src/BrowseComponent.tsx
@@ -29,7 +29,7 @@ const TableSortLabelWithoutMUI = ({
   children: any;
 }) => <div>TableSortLabel</div>;
 
-interface MuiColumnProps extends ColumnProps {
+interface IMuiColumnProps extends ColumnProps {
   cellContentRenderer?: any;
   buttons?: any;
   selector?: any;
@@ -39,7 +39,7 @@ interface MuiColumnProps extends ColumnProps {
 
 export interface IMuiVirtualizedTableProps {
   classes: any;
-  columns: MuiColumnProps[];
+  columns: IMuiColumnProps[];
   headerHeight: any;
   onRowClick: any;
   onRowMouseOver: any;
@@ -56,12 +56,7 @@ export interface IMuiVirtualizedTableProps {
   displayedRowCount: any;
 }
 
-export interface IMuiVirtualizedTableState {}
-
-class MuiVirtualizedTable extends React.PureComponent<
-  IMuiVirtualizedTableProps,
-  IMuiVirtualizedTableState
-> {
+class MuiVirtualizedTable extends React.PureComponent<IMuiVirtualizedTableProps> {
   static defaultProps: Partial<IMuiVirtualizedTableProps> = {
     headerHeight: 56,
     rowHeight: 56
@@ -89,7 +84,11 @@ class MuiVirtualizedTable extends React.PureComponent<
   cellRenderer = ({ cellData, columnIndex = null }: TableCellProps) => {
     const { columns } = this.props;
     let isPercent = false;
-    if (columnIndex != null && columns[columnIndex].percent) {
+    if (
+      columnIndex !== null &&
+      columnIndex !== undefined &&
+      columns[columnIndex].percent
+    ) {
       isPercent = columns[columnIndex].percent === true;
     }
     if (cellData !== undefined || cellData !== null) {
@@ -102,8 +101,14 @@ class MuiVirtualizedTable extends React.PureComponent<
   buttonRenderer = ({ columnIndex = null, rowData }: TableCellProps) => {
     const { columns } = this.props;
     // Retrieve event handler from column def
-    let handleClick = (dataId: number | string) => {};
-    if (columnIndex != null && columns[columnIndex].handleClick) {
+    let handleClick = (dataId: number | string) => {
+      // do nothing.
+    };
+    if (
+      columnIndex !== null &&
+      columnIndex !== undefined &&
+      columns[columnIndex].handleClick
+    ) {
       handleClick = columns[columnIndex].handleClick;
     }
     return (
@@ -136,7 +141,7 @@ class MuiVirtualizedTable extends React.PureComponent<
     };
 
     const inner =
-      !disableSort && sort != null ? (
+      !disableSort && sort !== null && sort !== undefined ? (
         <TableSortLabelWithoutMUI
           active={dataKey === sortBy}
           direction={direction[sortDirection]}
@@ -194,11 +199,14 @@ class MuiVirtualizedTable extends React.PureComponent<
                       buttons,
                       selector,
                       ...other
-                    }: MuiColumnProps,
+                    }: IMuiColumnProps,
                     index: number
                   ) => {
                     let renderer;
-                    if (cellContentRenderer != null) {
+                    if (
+                      cellContentRenderer !== null &&
+                      cellContentRenderer !== undefined
+                    ) {
                       renderer = (cellRendererProps: TableCellProps) =>
                         this.cellRenderer({
                           cellData: cellContentRenderer(cellRendererProps),
@@ -238,7 +246,7 @@ class MuiVirtualizedTable extends React.PureComponent<
   }
 }
 
-export interface BrowseComponentProps {
+export interface IBrowseComponentProps {
   features: any;
   handleClickFeature: any;
   handleZoomFeature: any;
@@ -256,7 +264,7 @@ function BrowseComponent({
   highlightFeature,
   isRetrievingMoreFeature,
   handleRetrieveMoreFeature
-}: BrowseComponentProps) {
+}: IBrowseComponentProps) {
   /**
    * Return an object with its id and all its properties
    */
diff --git a/src/CodeGenerator.ts b/src/CodeGenerator.ts
index b69c6ed..1145889 100644
--- a/src/CodeGenerator.ts
+++ b/src/CodeGenerator.ts
@@ -57,10 +57,10 @@ dag = EODataAccessGateway()
       additionnalParameters
         .filter(({ name, value }) => name !== '' && value !== '')
         .map(({ name, value }) => `  ${name}="${value}",`)
-        .join('\n') + `\n`;
+        .join('\n') + '\n';
   }
 
-  code += `)`;
+  code += ')';
   return code;
 };
 
diff --git a/src/DescriptionProductComponent.tsx b/src/DescriptionProductComponent.tsx
index f101dfb..6f40f5a 100644
--- a/src/DescriptionProductComponent.tsx
+++ b/src/DescriptionProductComponent.tsx
@@ -5,7 +5,7 @@
 
 import _ from 'lodash';
 import * as React from 'react';
-import { FeaturePropertie } from './types';
+import { IFeaturePropertie } from './types';
 
 const MEANPROPERTIES = [
   'platformSerialIdentifier',
@@ -33,7 +33,7 @@ export interface IProps {
 
 export interface IState {
   smallQuicklook: any;
-  featureProperties: FeaturePropertie[];
+  featureProperties: IFeaturePropertie[];
 }
 
 export default class DescriptionProductComponent extends React.Component<
@@ -64,11 +64,11 @@ export default class DescriptionProductComponent extends React.Component<
     }
   }
 
-  organizeProperties(): FeaturePropertie[] {
+  organizeProperties(): IFeaturePropertie[] {
     const { properties } = this.props.feature;
     const meanProperties: [string, any][] = [];
     MEANPROPERTIES.forEach(p => {
-      let fp = properties[p];
+      const fp = properties[p];
       if (fp !== undefined && fp !== null && fp !== '' && fp.length !== 0) {
         meanProperties.push([p, formatProperty(p, fp)]);
       }
@@ -89,7 +89,7 @@ export default class DescriptionProductComponent extends React.Component<
     ).sort();
 
     // Split keys on Upper letters and make a new capitalized string
-    const featureProperties: FeaturePropertie[] = meanProperties
+    const featureProperties: IFeaturePropertie[] = meanProperties
       .concat(otherProperties)
       .map(([k, v]: [string, any]) => ({
         key: _.capitalize(k.split(/(?=[A-Z])/).join(' ')),
@@ -117,7 +117,7 @@ export default class DescriptionProductComponent extends React.Component<
         </div>
         <h4>Metadata</h4>
         <table className="result-table">
-          {featureProperties.map(({ key, value }: FeaturePropertie) => (
+          {featureProperties.map(({ key, value }: IFeaturePropertie) => (
             <tr>
               <td>{key}</td>
               <td>
diff --git a/src/FormComponent.tsx b/src/FormComponent.tsx
index 1887c31..3204027 100644
--- a/src/FormComponent.tsx
+++ b/src/FormComponent.tsx
@@ -59,8 +59,8 @@ export const FormComponent: FC<IProps> = ({
 
   useEffect(() => {
     // Fetch product types
-    let _serverSettings = ServerConnection.makeSettings();
-    let _eodag_server = URLExt.join(
+    const _serverSettings = ServerConnection.makeSettings();
+    const _eodag_server = URLExt.join(
       _serverSettings.baseUrl,
       `${EODAG_SERVER_ADRESS}`
     );
@@ -111,7 +111,7 @@ export const FormComponent: FC<IProps> = ({
     setIsLoadingSearch(true);
     SearchService.search(1, data)
       .then(featureCollection => {
-        if (featureCollection?.features?.length == 0) {
+        if (featureCollection?.features?.length === 0) {
           throw new Error('No result found');
         } else {
           return featureCollection;
diff --git a/src/MapExtentComponent.tsx b/src/MapExtentComponent.tsx
index 9625131..5922310 100644
--- a/src/MapExtentComponent.tsx
+++ b/src/MapExtentComponent.tsx
@@ -8,19 +8,19 @@ import { Map, TileLayer, FeatureGroup } from 'react-leaflet';
 import { EditControl } from 'react-leaflet-draw';
 import { throttle } from 'lodash';
 import { EODAG_TILE_URL, EODAG_TILE_COPYRIGHT } from './config';
-import { Geometry } from './types';
+import { IGeometry } from './types';
 import { LeafletMouseEvent } from 'leaflet';
 
 export interface IProps {
-  onChange: (value: Geometry) => void;
-  geometry: Geometry;
+  onChange: (value: IGeometry) => void;
+  geometry: IGeometry;
 }
 
 export interface IState {
   lat: number;
   lon: number;
   zoom: number;
-  geometry: Geometry;
+  geometry: IGeometry;
 }
 
 export default class MapExtentComponent extends React.Component<
@@ -53,19 +53,18 @@ export default class MapExtentComponent extends React.Component<
    * Waiting the app to load before attaching an observer to watch for plugin width change
    */
   componentDidMount() {
-    const that = this;
-    const timer = setInterval(function () {
-      var observer = new MutationObserver(function (mutations) {
-        that.invalidateMapSize();
+    const timer = setInterval(() => {
+      const observer = new MutationObserver(mutations => {
+        this.invalidateMapSize();
       });
-      var target = document.querySelector('.jp-EodagWidget');
+      const target = document.querySelector('.jp-EodagWidget');
       if (target) {
         observer.observe(target, {
           attributes: true
         });
         clearInterval(timer);
       }
-      that.invalidateMapSize();
+      this.invalidateMapSize();
     }, 100);
   }
 
@@ -91,7 +90,7 @@ export default class MapExtentComponent extends React.Component<
       this.saveGeometry(e.target);
     } catch (TypeError) {
       // When clearAll `updateMapProperties` crash
-      const geometry: Geometry = undefined;
+      const geometry: IGeometry = undefined;
       this.setState(
         {
           geometry
@@ -127,7 +126,7 @@ export default class MapExtentComponent extends React.Component<
   getGeometry = (target: any) => {
     // I didn’t found a method from leaflet that returns a Multipolygon so we build it if there are more than one polygon
     const layers: any[] = [];
-    let geometry: Geometry;
+    let geometry: IGeometry;
     target.eachLayer((l: any) => layers.push(l.toGeoJSON?.()));
     const geo = layers
       .filter(e => e?.type === 'Feature')
diff --git a/src/SearchService.ts b/src/SearchService.ts
index 3be2c53..f9c3384 100644
--- a/src/SearchService.ts
+++ b/src/SearchService.ts
@@ -9,7 +9,7 @@ import { EODAG_SERVER_ADRESS } from './config';
 import { ServerConnection } from '@jupyterlab/services';
 import { URLExt } from '@jupyterlab/coreutils';
 import { formatDate } from './utils';
-import { IFormInput, SearchParameters } from './types';
+import { IFormInput, ISearchParameters } from './types';
 import _ from 'lodash';
 
 class SearchService {
@@ -18,8 +18,8 @@ class SearchService {
    * @returns the URL to fetch from the EODAG server to get products
    */
   getSearchURL(productType: string) {
-    let _serverSettings = ServerConnection.makeSettings();
-    let _eodag_server = URLExt.join(
+    const _serverSettings = ServerConnection.makeSettings();
+    const _eodag_server = URLExt.join(
       _serverSettings.baseUrl,
       `${EODAG_SERVER_ADRESS}`
     );
@@ -34,7 +34,7 @@ class SearchService {
    */
   search(page = 1, formValues: IFormInput) {
     const url = this.getSearchURL(formValues.productType);
-    let parameters: SearchParameters = {
+    let parameters: ISearchParameters = {
       dtstart: formValues.startDate
         ? formatDate(formValues.startDate)
         : undefined,
diff --git a/src/types.ts b/src/types.ts
index c285f99..be3e53f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -4,7 +4,7 @@
  */
 
 export interface IFormInput {
-  geometry: Geometry;
+  geometry: IGeometry;
   startDate: Date;
   endDate: Date;
   productType: string;
@@ -12,27 +12,27 @@ export interface IFormInput {
   additionnalParameters?: { name: string; value: string }[];
 }
 
-export interface SearchParameters {
+export interface ISearchParameters {
   dtstart?: string; // format: YYYY-MM-DD
   dtend?: string; // format: YYYY-MM-DD
   cloudCover?: number;
   page?: number;
-  geom?: Geometry;
+  geom?: IGeometry;
   [key: string]: any;
 }
 
-export interface Geometry {
+export interface IGeometry {
   type: string;
   coordinates: number[];
 }
-// export type FeaturePropertie = [string, any];
-export interface FeaturePropertie {
+// export type IFeaturePropertie = [string, any];
+export interface IFeaturePropertie {
   key: string;
   value: any;
 }
 
-// export type FeaturePropertie = [string, any];
-export interface FeaturePropertie {
+// export type IFeaturePropertie = [string, any];
+export interface IFeaturePropertie {
   key: string;
   value: any;
 }
diff --git a/src/utils.ts b/src/utils.ts
index 21465d9..5d5b1d6 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -8,7 +8,7 @@
  * @returns a string like YYYY-MM-DD
  */
 export function formatDate(date: Date): string {
-  var local = new Date(date);
+  const local = new Date(date);
   local.setMinutes(date.getMinutes() - date.getTimezoneOffset());
   return formatStringDate(local.toJSON());
 }


Test patch:
diff --git a/get_pypi_latest_version.sh b/get_pypi_latest_version.sh
new file mode 100755
index 0000000..bc52e4d
--- /dev/null
+++ b/get_pypi_latest_version.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+curl https://pypi.org/pypi/eodag/json | python -c \
+"import sys, json; v = list(json.load(sys.stdin)['releases'].keys()); v.sort(); print(v[-1]);"