├── .github ├── remark.yaml └── workflows │ ├── publish.yaml │ └── test.yaml ├── .gitignore ├── .pylintrc ├── .remarkignore ├── .remarkrc ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── 0001-record-architecture-decisions.md ├── 0002-use-case-definition.md ├── 0003-test-train-validation-split.md └── 0004-multiple-label-items.md ├── examples ├── collection_EuroSAT-subset-train.json └── item_EuroSAT-subset-train-sample-42-class-Residential.geojson ├── json-schema └── schema.json ├── package.json ├── pystac_ml_aoi ├── __init__.py └── extensions │ └── ml_aoi.py ├── requirements-dev.txt ├── requirements-sys.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── conftest.py ├── test_examples.py └── test_pystac_extension.py /.github/remark.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | # Check links 3 | - validate-links 4 | # Apply some recommended defaults for consistency 5 | - remark-preset-lint-consistent 6 | - remark-preset-lint-recommended 7 | - lint-no-html 8 | # General formatting 9 | - - remark-lint-emphasis-marker 10 | - '*' 11 | - remark-lint-hard-break-spaces 12 | - remark-lint-blockquote-indentation 13 | - remark-lint-no-consecutive-blank-lines 14 | - - remark-lint-maximum-line-length 15 | - 150 16 | # Code 17 | - remark-lint-fenced-code-flag 18 | - remark-lint-fenced-code-marker 19 | - remark-lint-no-shell-dollars 20 | - - remark-lint-code-block-style 21 | - 'fenced' 22 | # Headings 23 | - remark-lint-heading-increment 24 | - remark-lint-no-multiple-toplevel-headings 25 | - remark-lint-no-heading-punctuation 26 | - - remark-lint-maximum-heading-length 27 | - 70 28 | - - remark-lint-heading-style 29 | - atx 30 | - - remark-lint-no-shortcut-reference-link 31 | - false 32 | # Lists 33 | - remark-lint-list-item-bullet-indent 34 | - remark-lint-ordered-list-marker-style 35 | - remark-lint-ordered-list-marker-value 36 | - remark-lint-checkbox-character-style 37 | - - remark-lint-unordered-list-marker-style 38 | - '-' 39 | - - remark-lint-list-item-indent 40 | - space 41 | # Tables 42 | - remark-lint-table-pipes 43 | - remark-lint-no-literal-urls 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish JSON Schema 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Inject env variables 10 | uses: rlespinasse/github-slug-action@v3.x 11 | - uses: actions/checkout@v2 12 | - name: deploy JSON Schema for version ${{ env.GITHUB_REF_SLUG }} 13 | uses: peaceiris/actions-gh-pages@v3 14 | with: 15 | github_token: ${{ secrets.GITHUB_TOKEN }} 16 | publish_dir: json-schema 17 | destination_dir: ${{ env.GITHUB_REF_SLUG }} -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Check Documentation and Run Tests 2 | on: 3 | - push 4 | - pull_request 5 | - workflow_dispatch 6 | 7 | # cancel the current workflow if another commit was pushed on the same PR or reference 8 | # uses the GitHub workflow name to avoid collision with other workflows running on the same PR/reference 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | # see: https://github.com/fkirc/skip-duplicate-actions 15 | skip_duplicate: 16 | continue-on-error: true 17 | runs-on: ubuntu-latest 18 | outputs: 19 | should_skip: ${{ steps.skip_duplicate.outputs.should_skip && ! contains(github.ref, 'refs/tags') }} 20 | steps: 21 | - id: skip_check 22 | uses: fkirc/skip-duplicate-actions@master 23 | with: 24 | concurrent_skipping: "same_content_newer" 25 | skip_after_successful_duplicate: "true" 26 | do_not_skip: '["pull_request", "workflow_dispatch", "schedule", "release"]' 27 | 28 | # see: https://github.com/actions/setup-python 29 | tests: 30 | needs: skip_duplicate 31 | if: ${{ needs.skip_duplicate.outputs.should_skip != 'true' }} 32 | runs-on: ${{ matrix.os }} 33 | continue-on-error: ${{ matrix.allow-failure }} 34 | env: 35 | # override make command to install directly in active python 36 | CONDA_CMD: "" 37 | strategy: 38 | matrix: 39 | os: [ ubuntu-latest ] 40 | python-version: [ "3.9", "3.10", "3.11", "3.12" ] 41 | allow-failure: [ false ] 42 | test-case: [ test-unit-only ] 43 | include: 44 | # linter tests 45 | - os: ubuntu-latest 46 | python-version: "3.10" 47 | allow-failure: false 48 | test-case: check-only 49 | 50 | steps: 51 | - uses: actions/checkout@v2 52 | with: 53 | fetch-depth: "0" 54 | - name: Setup Python 55 | # skip python setup if running with docker 56 | if: ${{ matrix.test-case != 'test-docker' }} 57 | uses: actions/setup-python@v2 58 | with: 59 | python-version: "${{ matrix.python-version }}" 60 | - name: Parse Python Version 61 | id: python-semver 62 | run: | 63 | echo "::set-output name=major:$(echo ${{ matrix.python-version }} | cut -d '.' -f 1)" 64 | echo "::set-output name=minor:$(echo ${{ matrix.python-version }} | cut -d '.' -f 2)" 65 | - name: Install Dependencies 66 | # install package and dependencies directly, 67 | # skip sys/conda setup to use active python 68 | run: make install-dev version 69 | - name: Display Packages 70 | run: pip freeze 71 | - name: Display Environment Variables 72 | run: | 73 | hash -r 74 | env | sort 75 | - name: Run Tests 76 | run: make ${{ matrix.test-case }} 77 | 78 | deploy: 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/setup-node@v2 82 | with: 83 | node-version: 'lts/*' 84 | - uses: actions/checkout@v2 85 | - run: | 86 | npm install 87 | npm test 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | package-lock.json 4 | /node_modules 5 | [Mm]akefile.config 6 | **/__pycache__ 7 | *.egg-info 8 | *.py[cod] 9 | reports/ 10 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist=lxml.etree, 7 | schema_salad.sourceline, 8 | schema_salad.validate 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Python code to execute, usually for sys.path manipulation such as 19 | # pygtk.require(). 20 | #init-hook= 21 | 22 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 23 | # number of processors available to use. 24 | jobs=0 25 | 26 | # Control the amount of potential inferred values when inferring a single 27 | # object. This can help the performance when dealing with large functions or 28 | # complex, nested conditions. 29 | limit-inference-results=100 30 | 31 | # List of plugins (as comma separated values of python module names) to load, 32 | # usually to register additional checkers. 33 | # https://pylint.pycqa.org/en/latest/technical_reference/extensions.html 34 | load-plugins=pylint.extensions.docparams, 35 | pylint.extensions.mccabe, 36 | pylint_per_file_ignores 37 | 38 | # https://pylint.pycqa.org/en/latest/technical_reference/extensions.html#design-checker-options 39 | max-complexity = 24 40 | # FIXME: medium/lower-high complexity permitted, should focus toward <20 to facilitate testing and debugging 41 | 42 | # https://pylint.pycqa.org/en/latest/technical_reference/extensions.html#parameter-documentation-checker-options 43 | # allow unspecified items, but if the docstring is present, it should follow Sphinx format for documentation generation 44 | # see also 'disabled' W90XX warnings, we ignore only raised 'missing', but detect invalid documentation issues 45 | accept-no-param-doc = yes 46 | accept-no-raise-doc = yes 47 | accept-no-return-doc = yes 48 | accept-no-yields-doc = yes 49 | default-docstring-type = sphinx 50 | 51 | # Pickle collected data for later comparisons. 52 | persistent=yes 53 | 54 | # Specify a configuration file. 55 | #rcfile= 56 | 57 | # When enabled, pylint would attempt to guess common misconfiguration and emit 58 | # user-friendly hints instead of false-positive error messages. 59 | suggestion-mode=yes 60 | 61 | # Allow loading of arbitrary C extensions. Extensions are imported into the 62 | # active Python interpreter and may run arbitrary code. 63 | unsafe-load-any-extension=no 64 | 65 | 66 | [MESSAGES CONTROL] 67 | 68 | # Only show warnings with the listed confidence levels. Leave empty to show 69 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 70 | confidence= 71 | 72 | # Disable the message, report, category or checker with the given id(s). You 73 | # can either give multiple identifiers separated by comma (,) or put this 74 | # option multiple times (only on the command line, not in the configuration 75 | # file where it should appear only once). You can also use "--disable=all" to 76 | # disable everything first and then re-enable specific checks. For example, if 77 | # you want to run only the similarities checker, you can use "--disable=all 78 | # --enable=similarities". If you want to run only the classes checker, but have 79 | # no Warning level messages displayed, use "--disable=all --enable=classes 80 | # --disable=W". 81 | disable=C0111,missing-docstring, 82 | C0206,consider-using-dict-items, 83 | C0302,too-many-lines, 84 | C0412,ungrouped-imports, 85 | C0415,import-outside-toplevel, 86 | C1801,len-as-condition, 87 | E0015,unrecognized-option, 88 | E0401,import-error, 89 | E5110,django-not-configured, 90 | R0022,useless-option-value, 91 | R0201,no-self-use, 92 | R0205,useless-object-inheritance, 93 | # R0401 disabled to avoid unnecessary warnings on already handled circular imports 94 | # See issue : https://github.com/PyCQA/pylint/issues/850 95 | R0401,cyclic-import, 96 | R0801,duplicate-code, 97 | R0901,too-many-ancestors, 98 | R0904,too-many-public-methods, 99 | R0912,too-many-branches, 100 | R0914,too-many-locals, 101 | R0915,too-many-statements, 102 | R1705,no-else-return, 103 | R1710,inconsistent-return-statements, 104 | R1711,useless-return, 105 | R1721,unnecessary-comprehension, 106 | R1725,super-with-arguments, 107 | W0012,unknown-option-value, 108 | W0108,unnecessary-lambda, 109 | W0212,protected-access, 110 | W0232,no-init, 111 | W0235,useless-super-delegation, 112 | W0613,unused-argument, 113 | W0622,redefined-builtin, 114 | W0640,cell-var-from-loop, 115 | W0706,try-except-raise, 116 | W0707,raise-missing-from, 117 | W1508,invalid-envvar-default, 118 | W9005,multiple-constructor-doc, 119 | W9006,missing-raises-doc, 120 | W9011,missing-return-doc, 121 | W9012,missing-return-type-doc, 122 | W9013,missing-yield-doc, 123 | W9014,missing-yield-type-doc, 124 | W9015,missing-param-doc, 125 | W9016,missing-type-doc 126 | 127 | # note: (C0412, ungrouped-imports) is managed via isort tool, ignore false positives 128 | 129 | per-file-ignores = 130 | tests/*:R1729 131 | 132 | # Enable the message, report, category or checker with the given id(s). You can 133 | # either give multiple identifier separated by comma (,) or put this option 134 | # multiple time (only on the command line, not in the configuration file where 135 | # it should appear only once). See also the "--disable" option for examples. 136 | enable=c-extension-no-member 137 | 138 | 139 | [REPORTS] 140 | 141 | # Python expression which should return a score less than or equal to 10. You 142 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 143 | # which contain the number of messages in each category, as well as 'statement' 144 | # which is the total number of statements analyzed. This score is used by the 145 | # global evaluation report (RP0004). 146 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 147 | 148 | # Template used to display messages. This is a python new-style format string 149 | # used to format the message information. See doc for all details. 150 | #msg-template= 151 | 152 | # Set the output format. Available formats are text, parseable, colorized, json 153 | # and MSVC (visual studio). You can also give a reporter class, e.g. 154 | # mypackage.mymodule.MyReporterClass. 155 | output-format=colorized 156 | 157 | # Tells whether to display a full report or only the messages. 158 | reports=no 159 | 160 | # Activate the evaluation score. 161 | score=yes 162 | 163 | 164 | [REFACTORING] 165 | 166 | # Maximum number of nested blocks for function / method body 167 | max-nested-blocks=10 168 | 169 | # Complete name of functions that never returns. When checking for 170 | # inconsistent-return-statements if a never returning function is called then 171 | # it will be considered as an explicit return statement and no message will be 172 | # printed. 173 | never-returning-functions=sys.exit 174 | 175 | 176 | [TYPECHECK] 177 | 178 | # List of decorators that produce context managers, such as 179 | # contextlib.contextmanager. Add to this list to register other decorators that 180 | # produce valid context managers. 181 | contextmanager-decorators=contextlib.contextmanager 182 | 183 | # List of members which are set dynamically and missed by pylint inference 184 | # system, and so shouldn't trigger E1101 when accessed. Python regular 185 | # expressions are accepted. 186 | generated-members=str.value 187 | # note: 'str.value' is flagged by Enum members defined with strings 188 | 189 | # Tells whether missing members accessed in mixin class should be ignored. A 190 | # mixin class is detected if its name ends with "mixin" (case insensitive). 191 | ignore-mixin-members=yes 192 | 193 | # Tells whether to warn about missing members when the owner of the attribute 194 | # is inferred to be None. 195 | ignore-none=yes 196 | 197 | # This flag controls whether pylint should warn about no-member and similar 198 | # checks whenever an opaque object is returned when inferring. The inference 199 | # can return multiple potential results while evaluating a Python object, but 200 | # some branches might not be evaluated, which results in partial inference. In 201 | # that case, it might be useful to still emit no-member and other checks for 202 | # the rest of the inferred objects. 203 | ignore-on-opaque-inference=yes 204 | 205 | # List of class names for which member attributes should not be checked (useful 206 | # for classes with dynamically set attributes). This supports the use of 207 | # qualified names. 208 | ignored-classes=optparse.Values,thread._local,_thread._local,str 209 | 210 | # List of module names for which member attributes should not be checked 211 | # (useful for modules/projects where namespaces are manipulated during runtime 212 | # and thus existing member attributes cannot be deduced by static analysis). It 213 | # supports qualified module names, as well as Unix pattern matching. 214 | #ignored-modules= 215 | 216 | # Show a hint with possible names when a member name was not found. The aspect 217 | # of finding the hint is based on edit distance. 218 | missing-member-hint=yes 219 | 220 | # The minimum edit distance a name should have in order to be considered a 221 | # similar match for a missing member name. 222 | missing-member-hint-distance=1 223 | 224 | # The total number of similar names that should be taken in consideration when 225 | # showing a hint for a missing member. 226 | missing-member-max-choices=1 227 | 228 | # List of decorators that change the signature of a decorated function. 229 | signature-mutators= 230 | 231 | 232 | [VARIABLES] 233 | 234 | # List of additional names supposed to be defined in builtins. Remember that 235 | # you should avoid defining new builtins when possible. 236 | additional-builtins= 237 | 238 | # Tells whether unused global variables should be treated as a violation. 239 | allow-global-unused-variables=yes 240 | 241 | # List of strings which can identify a callback function by name. A callback 242 | # name must start or end with one of those strings. 243 | callbacks=cb_, 244 | _cb 245 | 246 | # A regular expression matching the name of dummy variables (i.e. expected to 247 | # not be used). 248 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 249 | 250 | # Argument names that match this expression will be ignored. Default to name 251 | # with leading underscore. 252 | ignored-argument-names=_.*|^ignored_|^unused_ 253 | 254 | # Tells whether we should check for unused import in __init__ files. 255 | init-import=yes 256 | 257 | # List of qualified module names which can have objects that can redefine 258 | # builtins. 259 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 260 | 261 | 262 | [SPELLING] 263 | 264 | # Limits count of emitted suggestions for spelling mistakes. 265 | max-spelling-suggestions=4 266 | 267 | # Spelling dictionary name. Available dictionaries: none. To make it work, 268 | # install the python-enchant package. 269 | spelling-dict= 270 | 271 | # List of comma separated words that should not be checked. 272 | spelling-ignore-words= 273 | 274 | # A path to a file that contains the private dictionary; one word per line. 275 | spelling-private-dict-file= 276 | 277 | # Tells whether to store unknown words to the private dictionary (see the 278 | # --spelling-private-dict-file option) instead of raising a message. 279 | spelling-store-unknown-words=no 280 | 281 | 282 | [LOGGING] 283 | 284 | # Format style used to check logging format string. `old` means using % 285 | # formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. 286 | logging-format-style=old 287 | 288 | # Logging modules to check that the string format arguments are in logging 289 | # function parameter format. 290 | logging-modules=logging 291 | 292 | 293 | [STRING] 294 | 295 | # This flag controls whether the implicit-str-concat-in-sequence should 296 | # generate a warning on implicit string concatenation in sequences defined over 297 | # several lines. 298 | check-str-concat-over-line-jumps=no 299 | 300 | # plugin: 301 | # https://pypi.python.org/pypi/pylint-quotes 302 | string-quote=double 303 | triple-quote=double 304 | docstring-quote=double 305 | 306 | 307 | [FORMAT] 308 | 309 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 310 | expected-line-ending-format= 311 | 312 | # Regexp for a line that is allowed to be longer than the limit. 313 | # - Ignore HTTP(S) URL 314 | # - Ignore RST references (e.g.: long method path) 315 | # Both could be indented (e.g.: within a docstring, class, function, etc.) 316 | ignore-long-lines=^\s*(# )??$|^\s*\:\w+\:\`\S+\`$ 317 | 318 | # Number of spaces of indent required inside a hanging or continued line. 319 | indent-after-paren=4 320 | 321 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 322 | # tab). 323 | indent-string=' ' 324 | 325 | # Maximum number of characters on a single line. 326 | max-line-length=120 327 | 328 | # Maximum number of lines in a module. 329 | max-module-lines=1000 330 | 331 | # List of optional constructs for which whitespace checking is disabled. `dict- 332 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 333 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 334 | # `empty-line` allows space-only lines. 335 | no-space-check=trailing-comma, 336 | dict-separator 337 | 338 | # Allow the body of a class to be on the same line as the declaration if body 339 | # contains single statement. 340 | single-line-class-stmt=no 341 | 342 | # Allow the body of an if to be on the same line as the test if there is no 343 | # else. 344 | single-line-if-stmt=no 345 | 346 | 347 | [BASIC] 348 | 349 | # Naming style matching correct argument names. 350 | argument-naming-style=snake_case 351 | 352 | # Regular expression matching correct argument names. Overrides argument- 353 | # naming-style. 354 | #argument-rgx= 355 | 356 | # Naming style matching correct attribute names. 357 | attr-naming-style=snake_case 358 | 359 | # Regular expression matching correct attribute names. Overrides attr-naming- 360 | # style. 361 | #attr-rgx= 362 | 363 | # Bad variable names which should always be refused, separated by a comma. 364 | bad-names=foo, 365 | bar, 366 | baz, 367 | toto, 368 | tutu, 369 | tata 370 | 371 | # Naming style matching correct class attribute names. 372 | class-attribute-naming-style=any 373 | 374 | # Regular expression matching correct class attribute names. Overrides class- 375 | # attribute-naming-style. 376 | #class-attribute-rgx= 377 | 378 | # Naming style matching correct class names. 379 | class-naming-style=PascalCase 380 | 381 | # Regular expression matching correct class names. Overrides class-naming-style. 382 | # Allow typing definitions that are matched as 'classes' to have slightly more versatile names. 383 | class-rgx=((_{0,2}[A-Z][a-zA-Z0-9]+)|((CWL|PKG|WPS|OAS|JSON|IO|ANY)_[a-zA-Z0-9_]+Types?))$ 384 | 385 | typealias-rgx=_{0,2}(?!T[A-Z]|Type)((CWL|PKG|WPS|OAS|JSON|IO|ANY)_)?[A-Z]+[a-z0-9]+(?:[A-Z][a-z0-9]+)*$ 386 | 387 | # Naming style matching correct constant names. 388 | const-naming-style=UPPER_CASE 389 | 390 | # Regular expression matching correct constant names. Overrides const-naming- 391 | # style. 392 | #const-rgx= 393 | 394 | # Minimum line length for functions/classes that require docstrings, shorter 395 | # ones are exempt. 396 | docstring-min-length=-1 397 | 398 | # Naming style matching correct function names. 399 | function-naming-style=snake_case 400 | 401 | # Regular expression matching correct function names. Overrides function- 402 | # naming-style. 403 | #function-rgx= 404 | 405 | # Good variable names which should always be accepted, separated by a comma. 406 | good-names=i,j,k,v,kv,ex,x,y,z,f,h,db,kw,dt,q,ns,id,s3,to,_ 407 | 408 | # Include a hint for the correct naming format with invalid-name. 409 | include-naming-hint=yes 410 | 411 | # Naming style matching correct inline iteration names. 412 | inlinevar-naming-style=any 413 | 414 | # Regular expression matching correct inline iteration names. Overrides 415 | # inlinevar-naming-style. 416 | #inlinevar-rgx= 417 | 418 | # Naming style matching correct method names. 419 | method-naming-style=snake_case 420 | 421 | # Regular expression matching correct method names. Overrides method-naming- 422 | # style. 423 | #method-rgx= 424 | 425 | # Naming style matching correct module names. 426 | module-naming-style=snake_case 427 | 428 | # Regular expression matching correct module names. Overrides module-naming- 429 | # style. 430 | #module-rgx= 431 | 432 | # Colon-delimited sets of names that determine each other's naming style when 433 | # the name regexes allow several styles. 434 | name-group= 435 | 436 | # Regular expression which should only match function or class names that do 437 | # not require a docstring. 438 | no-docstring-rgx=^_ 439 | 440 | # List of decorators that produce properties, such as abc.abstractproperty. Add 441 | # to this list to register other decorators that produce valid properties. 442 | # These decorators are taken in consideration only for invalid-name. 443 | property-classes=abc.abstractproperty 444 | 445 | # Naming style matching correct variable names. 446 | variable-naming-style=snake_case 447 | 448 | # Regular expression matching correct variable names. Overrides variable- 449 | # naming-style. 450 | #variable-rgx= 451 | 452 | 453 | [MISCELLANEOUS] 454 | 455 | # List of note tags to take in consideration, separated by a comma. 456 | notes=HACK 457 | ## FIXME,TODO, 458 | 459 | 460 | [SIMILARITIES] 461 | 462 | # Ignore comments when computing similarities. 463 | ignore-comments=yes 464 | 465 | # Ignore docstrings when computing similarities. 466 | ignore-docstrings=yes 467 | 468 | # Ignore imports when computing similarities. 469 | ignore-imports=yes 470 | 471 | # Minimum lines number of a similarity. 472 | min-similarity-lines=10 473 | 474 | 475 | [CLASSES] 476 | 477 | # List of method names used to declare (i.e. assign) instance attributes. 478 | defining-attr-methods=__init__, 479 | __new__, 480 | setUp, 481 | __post_init__ 482 | 483 | # List of member names, which should be excluded from the protected access 484 | # warning. 485 | exclude-protected=_asdict, 486 | _fields, 487 | _replace, 488 | _source, 489 | _make 490 | 491 | # List of valid names for the first argument in a class method. 492 | valid-classmethod-first-arg=cls 493 | 494 | # List of valid names for the first argument in a metaclass class method. 495 | valid-metaclass-classmethod-first-arg=cls 496 | 497 | 498 | [IMPORTS] 499 | 500 | # List of modules that can be imported at any level, not just the top level 501 | # one. 502 | allow-any-import-level= 503 | 504 | # Allow wildcard imports from modules that define __all__. 505 | allow-wildcard-with-all=no 506 | 507 | # Analyse import fallback blocks. This can be used to support both Python 2 and 508 | # 3 compatible code, which means that the block might have code that exists 509 | # only in one or another interpreter, leading to false positives when analysed. 510 | analyse-fallback-blocks=no 511 | 512 | # Deprecated modules which should not be used, separated by a comma. 513 | deprecated-modules=optparse,tkinter.tix 514 | 515 | # Create a graph of external dependencies in the given file (report RP0402 must 516 | # not be disabled). 517 | ext-import-graph= 518 | 519 | # Create a graph of every (i.e. internal and external) dependencies in the 520 | # given file (report RP0402 must not be disabled). 521 | import-graph= 522 | 523 | # Create a graph of internal dependencies in the given file (report RP0402 must 524 | # not be disabled). 525 | int-import-graph= 526 | 527 | # Force import order to recognize a module as part of the standard 528 | # compatibility libraries. 529 | known-standard-library=posixpath,typing,typing_extensions 530 | 531 | # Force import order to recognize a module as part of a third party library. 532 | known-third-party=enchant,cornice_swagger,cwltool,cwt,docker 533 | 534 | # Couples of modules and preferred modules, separated by a comma. 535 | preferred-modules= 536 | 537 | 538 | [DESIGN] 539 | 540 | # Maximum number of arguments for function / method. 541 | max-args=20 542 | 543 | # Maximum number of attributes for a class (see R0902). 544 | max-attributes=20 545 | 546 | # Maximum number of boolean expressions in an if statement (see R0916). 547 | max-bool-expr=20 548 | 549 | # Maximum number of branch for function / method body. 550 | max-branches=20 551 | 552 | # Maximum number of locals for function / method body. 553 | max-locals=20 554 | 555 | # Maximum number of parents for a class (see R0901). 556 | max-parents=10 557 | 558 | # Maximum number of public methods for a class (see R0904). 559 | max-public-methods=20 560 | 561 | # Maximum number of return / yield for function / method body. 562 | max-returns=20 563 | 564 | # Maximum number of statements in function / method body. 565 | max-statements=100 566 | 567 | # Minimum number of public methods for a class (see R0903). 568 | min-public-methods=0 569 | 570 | 571 | [EXCEPTIONS] 572 | 573 | # Exceptions that will emit a warning when being caught. Defaults to 574 | # "BaseException, Exception". 575 | overgeneral-exceptions=builtins.BaseException 576 | -------------------------------------------------------------------------------- /.remarkignore: -------------------------------------------------------------------------------- 1 | # To save time scanning 2 | .idea/ 3 | .vscode/ 4 | *.egg-info/ 5 | downloads/ 6 | env/ 7 | 8 | # actual items to ignore 9 | .pytest_cache/ 10 | node_modules/ 11 | docs/_build/ 12 | docs/build/ 13 | reports/ 14 | -------------------------------------------------------------------------------- /.remarkrc: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "bullet": "-", 4 | "fence": "`", 5 | "fences": "true", 6 | "listItemIndent": "mixed", 7 | "incrementListMarker": "true", 8 | "resourceLink": "true", 9 | "rule": "-" 10 | }, 11 | "plugins": [ 12 | "remark-gfm", 13 | "remark-preset-lint-markdown-style-guide", 14 | "remark-preset-lint-recommended", 15 | "remark-lint-list-item-spacing", 16 | "remark-lint-list-item-content-indent", 17 | "remark-lint-checkbox-content-indent", 18 | ["lint-fenced-code-marker", "`"], 19 | ["lint-list-item-indent", "space"], 20 | ["lint-list-item-spacing", {"checkBlanks": true}], 21 | ["lint-maximum-line-length", 120], 22 | ["lint-ordered-list-marker-style", "."], 23 | ["lint-ordered-list-marker-value", "ordered"], 24 | ["lint-unordered-list-marker-style", "consistent"] 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Changelog 4 | 5 | All notable changes to this project will be documented in this file. 6 | 7 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 8 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 9 | 10 | [Unreleased](https://github.com/stac-extensions/ml-aoi/compare/v0.2.0...HEAD) (latest) 11 | --------------------------------------------------------------------------------------- 12 | 13 | ### Added 14 | - n/a 15 | 16 | ### Changed 17 | - n/a 18 | 19 | ### Deprecated 20 | - n/a 21 | 22 | ### Removed 23 | - n/a 24 | 25 | ### Fixed 26 | - n/a 27 | 28 | .. _changes_0.2.0: 29 | 30 | [v0.2.0](https://github.com/stac-extensions/ml-aoi/tree/v0.2.0) (2024-03-28) 31 | --------------------------------------------------------------------------------------- 32 | 33 | ### Added 34 | - Add `pystac_ml_aoi` Python package to provide similar capabilities to 35 | [`pystac.extensions`](https://github.com/stac-utils/pystac/tree/main/pystac/extensions) for ML-AOI. 36 | - Add GitHub Actions CI Workflow. 37 | - Add unittests for validating `pystac_ml_aoi` utilities and definitions. 38 | - Add unittests for validating `examples` STAC Collections and STAC Items against ML-AOI JSON-schema. 39 | 40 | ### Changed 41 | - n/a 42 | 43 | ### Deprecated 44 | - n/a 45 | 46 | ### Removed 47 | - n/a 48 | 49 | ### Fixed 50 | - Fix syntax and format of all Markdown files. 51 | 52 | [v0.1.0](https://github.com/stac-extensions/ml-aoi/tree/v0.1.0) (2021-04-29) 53 | --------------------------------------------------------------------------------------- 54 | 55 | Initial independent release. 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RELEASE := master 2 | 3 | # Included custom configs change the value of MAKEFILE_LIST 4 | # Extract the required reference beforehand so we can use it for help target 5 | MAKEFILE_NAME := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)) 6 | # Include custom config if it is available 7 | -include Makefile.config 8 | 9 | # Application 10 | APP_ROOT := $(abspath $(lastword $(MAKEFILE_NAME))/..) 11 | APP_NAME := $(shell basename $(APP_ROOT)) 12 | APP_PKG := pystac_ml_aoi 13 | APP_VERSION ?= 0.2.0 14 | 15 | # guess OS (Linux, Darwin,...) 16 | OS_NAME := $(shell uname -s 2>/dev/null || echo "unknown") 17 | CPU_ARCH := $(shell uname -m 2>/dev/null || uname -p 2>/dev/null || echo "unknown") 18 | 19 | # conda 20 | CONDA_CMD ?= __EMPTY__ 21 | CONDA_ENV ?= $(APP_NAME) 22 | CONDA_HOME ?= $(HOME)/.conda 23 | CONDA_ENVS_DIR ?= $(CONDA_HOME)/envs 24 | CONDA_ENV_PATH := $(CONDA_ENVS_DIR)/$(CONDA_ENV) 25 | ifneq ($(CONDA_CMD),__EMPTY__) 26 | CONDA_CMD := 27 | CONDA_BIN := 28 | CONDA_ENV := 29 | CONDA_ENV_MODE := [using overridden conda command] 30 | else 31 | CONDA_CMD := 32 | # allow pre-installed conda in Windows bash-like shell 33 | ifeq ($(findstring MINGW,$(OS_NAME)),MINGW) 34 | CONDA_BIN_DIR ?= $(CONDA_HOME)/Scripts 35 | else 36 | CONDA_BIN_DIR ?= $(CONDA_HOME)/bin 37 | endif 38 | CONDA_BIN ?= $(CONDA_BIN_DIR)/conda 39 | CONDA_ENV_REAL_TARGET_PATH := $(realpath $(CONDA_ENV_PATH)) 40 | CONDA_ENV_REAL_ACTIVE_PATH := $(realpath ${CONDA_PREFIX}) 41 | 42 | # environment already active - use it directly 43 | ifneq ("$(CONDA_ENV_REAL_ACTIVE_PATH)", "") 44 | CONDA_ENV_MODE := [using active environment] 45 | CONDA_ENV := $(notdir $(CONDA_ENV_REAL_ACTIVE_PATH)) 46 | CONDA_CMD := 47 | endif 48 | # environment not active but it exists - activate and use it 49 | ifneq ($(CONDA_ENV_REAL_TARGET_PATH), "") 50 | CONDA_ENV := $(notdir $(CONDA_ENV_REAL_TARGET_PATH)) 51 | endif 52 | # environment not active and not found - create, activate and use it 53 | ifeq ("$(CONDA_ENV)", "") 54 | CONDA_ENV := $(APP_NAME) 55 | endif 56 | # update paths for environment activation 57 | ifeq ("$(CONDA_ENV_REAL_ACTIVE_PATH)", "") 58 | CONDA_ENV_MODE := [will activate environment] 59 | CONDA_CMD := source "$(CONDA_BIN_DIR)/activate" "$(CONDA_ENV)"; 60 | endif 61 | endif 62 | DOWNLOAD_CACHE ?= $(APP_ROOT)/downloads 63 | PYTHON_VERSION ?= `python -c 'import platform; print(platform.python_version())'` 64 | PYTHON_VERSION_MAJOR := $(shell echo $(PYTHON_VERSION) | cut -f 1 -d '.') 65 | PYTHON_VERSION_MINOR := $(shell echo $(PYTHON_VERSION) | cut -f 2 -d '.') 66 | PYTHON_VERSION_PATCH := $(shell echo $(PYTHON_VERSION) | cut -f 3 -d '.' | cut -f 1 -d ' ') 67 | PIP_USE_FEATURE := `python -c '\ 68 | import pip; \ 69 | try: \ 70 | from packaging.version import Version \ 71 | except ImportError: \ 72 | from distutils.version import LooseVersion as Version \ 73 | print(Version(pip.__version__) < Version("21.0"))'` 74 | PIP_XARGS ?= 75 | ifeq ("$(PIP_USE_FEATURE)", "True") 76 | PIP_XARGS := --use-feature=2020-resolver $(PIP_XARGS) 77 | endif 78 | 79 | # choose conda installer depending on your OS 80 | CONDA_URL = https://repo.continuum.io/miniconda 81 | ifeq ("$(OS_NAME)", "Linux") 82 | FN := Miniconda3-latest-Linux-x86_64.sh 83 | else ifeq ("$(OS_NAME)", "Darwin") 84 | FN := Miniconda3-latest-MacOSX-x86_64.sh 85 | else 86 | FN := unknown 87 | endif 88 | 89 | # Tests 90 | REPORTS_DIR := $(APP_ROOT)/reports 91 | 92 | # end of configuration 93 | 94 | .DEFAULT_GOAL := help 95 | 96 | ## -- Informative targets ------------------------------------------------------------------------------------------- ## 97 | 98 | .PHONY: all 99 | all: help 100 | 101 | # Auto documented help targets & sections from comments 102 | # - detects lines marked by double octothorpe (#), then applies the corresponding target/section markup 103 | # - target comments must be defined after their dependencies (if any) 104 | # - section comments must have at least a double dash (-) 105 | # 106 | # Original Reference: 107 | # https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 108 | # Formats: 109 | # https://misc.flogisoft.com/bash/tip_colors_and_formatting 110 | _SECTION := \033[34m 111 | _TARGET := \033[36m 112 | _NORMAL := \033[0m 113 | .PHONY: help 114 | # note: use "\#\#" to escape results that would self-match in this target's search definition 115 | help: ## print this help message (default) 116 | @echo "$(_SECTION)=======================================$(_NORMAL)" 117 | @echo "$(_SECTION) $(APP_NAME) help $(_NORMAL)" 118 | @echo "$(_SECTION)=======================================$(_NORMAL)" 119 | @echo "Please use 'make ' where is one of below options." 120 | @echo "" 121 | @echo "NOTE:" 122 | @echo " Targets suffixed '-only' can be called as ' to run setup before their main operation." 123 | @echo "" 124 | # @grep -E '^[a-zA-Z_-]+:.*?\#\# .*$$' $(MAKEFILE_LIST) \ 125 | # | awk 'BEGIN {FS = ":.*?\#\# "}; {printf " $(_TARGET)%-24s$(_NORMAL) %s\n", $$1, $$2}' 126 | @grep -E '\#\#.*$$' "$(APP_ROOT)/$(MAKEFILE_NAME)" \ 127 | | awk ' BEGIN {FS = "(:|\-\-\-)+.*?\#\# "}; \ 128 | /\--/ {printf "$(_SECTION)%s$(_NORMAL)\n", $$1;} \ 129 | /:/ {printf " $(_TARGET)%-24s$(_NORMAL) %s\n", $$1, $$2} \ 130 | ' 131 | 132 | .PHONY: targets 133 | targets: help 134 | 135 | .PHONY: version 136 | version: ## display current version 137 | @-echo "$(APP_NAME) version: $(APP_VERSION)" 138 | 139 | .PHONY: info 140 | info: ## display make information 141 | @echo "Makefile configuration details:" 142 | @echo " OS Name $(OS_NAME)" 143 | @echo " CPU Architecture $(CPU_ARCH)" 144 | @echo " Conda Home $(CONDA_HOME)" 145 | @echo " Conda Prefix $(CONDA_ENV_PATH)" 146 | @echo " Conda Env Name $(CONDA_ENV)" 147 | @echo " Conda Env Path $(CONDA_ENV_REAL_ACTIVE_PATH)" 148 | @echo " Conda Binary $(CONDA_BIN)" 149 | @echo " Conda Activation $(CONDA_ENV_MODE)" 150 | @echo " Conda Command $(CONDA_CMD)" 151 | @echo " Application Name $(APP_NAME)" 152 | @echo " Application Root $(APP_ROOT)" 153 | @echo " Download Cache $(DOWNLOAD_CACHE)" 154 | @echo " Docker Repository $(DOCKER_REPO)" 155 | 156 | .PHONY: fixme-list-only 157 | fixme-list-only: mkdir-reports ## list all FIXME/TODO/HACK items that require attention in the code 158 | @echo "Listing code that requires fixes..." 159 | @echo '[MISCELLANEOUS]\nnotes=FIXME,TODO,HACK' > "$(REPORTS_DIR)/fixmerc" 160 | @bash -c '$(CONDA_CMD) \ 161 | pylint \ 162 | --disable=all,use-symbolic-message-instead --enable=miscellaneous,W0511 \ 163 | --score n --persistent n \ 164 | --rcfile="$(REPORTS_DIR)/fixmerc" \ 165 | -f colorized \ 166 | "$(APP_ROOT)/$(APP_PKG)" "$(APP_ROOT)/tests" \ 167 | 1> >(tee "$(REPORTS_DIR)/fixme.txt")' 168 | 169 | .PHONY: fixme-list 170 | fixme-list: install-dev fixme-list-only ## list all FIXME/TODO/HACK items with pre-installation of dependencies 171 | 172 | ## -- Conda targets ------------------------------------------------------------------------------------------------- ## 173 | 174 | .PHONY: conda-base 175 | conda-base: ## obtain and install a missing conda distribution 176 | @echo "Validating conda installation..." 177 | @test -f "$(CONDA_BIN)" || test -d "$(DOWNLOAD_CACHE)" || \ 178 | (echo "Creating download directory: $(DOWNLOAD_CACHE)" && mkdir -p "$(DOWNLOAD_CACHE)") 179 | @test -f "$(CONDA_BIN)" || test -f "$(DOWNLOAD_CACHE)/$(FN)" || \ 180 | (echo "Fetching conda distribution from: $(CONDA_URL)/$(FN)" && \ 181 | curl "$(CONDA_URL)/$(FN)" --insecure --location --output "$(DOWNLOAD_CACHE)/$(FN)") 182 | @test -f "$(CONDA_BIN)" || \ 183 | (bash "$(DOWNLOAD_CACHE)/$(FN)" -b -u -p "$(CONDA_HOME)" && \ 184 | echo "Make sure to add '$(CONDA_BIN_DIR)' to your PATH variable in '~/.bashrc'.") 185 | 186 | .PHONY: conda-clean 187 | clean-clean: ## remove the conda environment 188 | @echo "Removing conda env '$(CONDA_ENV)'" 189 | @-test -d "$(CONDA_ENV_PATH)" && "$(CONDA_BIN)" remove -n "$(CONDA_ENV)" --yes --all 190 | 191 | .PHONY: conda-config 192 | conda-config: conda-base ## setup configuration of the conda environment 193 | @echo "Updating conda configuration..." 194 | @ "$(CONDA_BIN)" config --add envs_dirs "$(CONDA_ENVS_DIR)" 195 | @ "$(CONDA_BIN)" config --set ssl_verify true 196 | @ "$(CONDA_BIN)" config --set channel_priority true 197 | @ "$(CONDA_BIN)" config --set auto_update_conda false 198 | @ "$(CONDA_BIN)" config --add channels defaults 199 | @ "$(CONDA_BIN)" config --append channels conda-forge 200 | 201 | .PHONY: conda-install 202 | conda-install: 203 | @[ -z "$(CONDA_ENV)" ] && echo "Skipping conda environment setup [CONDA_ENV empty]" || ( \ 204 | echo "Setup conda environment..."; \ 205 | $(MAKE) -C "$(APP_ROOT)" conda-env \ 206 | ) 207 | 208 | .PHONY: conda-env 209 | conda-env: conda-base conda-config ## create the conda environment 210 | @test -d "$(CONDA_ENV_PATH)" || \ 211 | (echo "Creating conda environment at '$(CONDA_ENV_PATH)'..." && \ 212 | "$(CONDA_HOME)/bin/conda" create -y -n "$(CONDA_ENV)" python=$(PYTHON_VERSION)) 213 | 214 | .PHONY: conda-pinned 215 | conda-pinned: conda-env ## pin the conda version 216 | @echo "Update pinned conda packages..." 217 | @-test -d $(CONDA_ENV_PATH) && test -f $(CONDA_PINNED) && \ 218 | cp -f "$(CONDA_PINNED)" "$(CONDA_ENV_PATH)/conda-meta/pinned" 219 | 220 | .PHONY: conda-env-export 221 | conda-env-export: ## export the conda environment 222 | @echo "Exporting conda environment..." 223 | @test -d $(CONDA_ENV_PATH) && "$(CONDA_BIN)" env export -n $(CONDA_ENV) -f environment.yml 224 | 225 | ## -- Build targets ------------------------------------------------------------------------------------------------- ## 226 | 227 | .PHONY: install 228 | install: install-run install-pip ## alias for 'install-all' target 229 | 230 | .PHONY: install-run 231 | install-run: conda-install install-sys install-pkg install-raw ## install requirements and application to run locally 232 | 233 | .PHONY: install-all 234 | install-all: conda-install install-sys install-pkg install-pip install-dev ## install application with all dependencies 235 | 236 | .PHONY: install-doc 237 | install-doc: install-pip ## install documentation dependencies 238 | @test -f "$(APP_ROOT)/requirements-doc.txt" && ( \ 239 | echo "Installing development packages with pip..." && \ 240 | bash -c '$(CONDA_CMD) pip install $(PIP_XARGS) -r "$(APP_ROOT)/requirements-doc.txt"' && \ 241 | echo "Install with pip complete. Run documentation generation with 'make docs' target." \ 242 | ) || echo "No documentation requirements to install." 243 | 244 | .PHONY: install-dev 245 | install-dev: install-run install-npm-remarklint ## install development and test dependencies 246 | @test -f "$(APP_ROOT)/requirements-dev.txt" && ( \ 247 | echo "Installing development packages with pip..." && \ 248 | bash -c '$(CONDA_CMD) pip install $(PIP_XARGS) -r "$(APP_ROOT)/requirements-dev.txt"' && \ 249 | echo "Install with pip complete. Test service with 'make test*' variations." \ 250 | ) || echo "No development requirements to install." 251 | 252 | .PHONY: install-pkg 253 | install-pkg: install-pip ## install application package dependencies 254 | @test -f "$(APP_ROOT)/requirements.txt" && ( \ 255 | echo "Installing base packages with pip..." && \ 256 | bash -c "$(CONDA_CMD) pip install $(PIP_XARGS) -r "$(APP_ROOT)/requirements.txt" --no-cache-dir" && \ 257 | echo "Install with pip complete." \ 258 | ) || echo "No application requirements to install." 259 | 260 | # don't use 'PIP_XARGS' in this case since extra features could not yet be supported by pip being installed/updated 261 | .PHONY: install-sys 262 | install-sys: ## install system dependencies and required installers/runners 263 | @test -f "$(APP_ROOT)/requirements-sys.txt" && ( \ 264 | echo "Installing system dependencies..." && \ 265 | bash -c '$(CONDA_CMD) pip install --upgrade -r "$(APP_ROOT)/requirements-sys.txt"' && \ 266 | echo "Install with pip complete." \ 267 | ) || echo "No system requirements to install." 268 | 269 | .PHONY: install-pip 270 | install-pip: ## install application as a package to allow import from another python package 271 | @echo "Installing package with pip..." 272 | @-bash -c '$(CONDA_CMD) pip install $(PIP_XARGS) --upgrade -e "$(APP_ROOT)" --no-cache' 273 | @echo "Install with pip complete." 274 | 275 | .PHONY: install-raw 276 | install-raw: ## install without any requirements or dependencies (suppose everything is setup) 277 | @echo "Installing package without dependencies..." 278 | @bash -c '$(CONDA_CMD) pip install $(PIP_XARGS) -e "$(APP_ROOT)" --no-deps' 279 | @echo "Install package complete." 280 | 281 | # install locally to ensure they can be found by config extending them 282 | .PHONY: install-npm 283 | install-npm: ## install npm package manager and dependencies if they cannot be found 284 | @[ -f "$(shell which npm)" ] || ( \ 285 | echo "Binary package manager npm not found. Attempting to install it."; \ 286 | apt-get install npm \ 287 | ) 288 | 289 | .PHONY: install-npm-remarklint 290 | install-npm-remarklint: install-npm ## install remark-lint dependency for 'check-md' target using npm 291 | @[ `npm ls 2>/dev/null | grep remark-lint | wc -l` = 1 ] || ( \ 292 | echo "Install required dependencies for Markdown checks." && \ 293 | npm install --save-dev \ 294 | remark-lint \ 295 | remark-gfm \ 296 | remark-cli \ 297 | remark-lint-maximum-line-length \ 298 | remark-lint-checkbox-content-indent \ 299 | remark-preset-lint-recommended \ 300 | remark-preset-lint-markdown-style-guide \ 301 | ) 302 | 303 | ## -- Cleanup targets ----------------------------------------------------------------------------------------------- ## 304 | 305 | .PHONY: clean 306 | clean: clean-all ## alias for 'clean-all' target 307 | 308 | .PHONY: clean-all 309 | clean-all: clean-build clean-cache clean-docs-dirs clean-src clean-reports clean-test ## run all cleanup targets 310 | 311 | .PHONY: clean-build 312 | clean-build: ## remove the temporary build files 313 | @echo "Removing build files..." 314 | @-rm -fr "$(APP_ROOT)/eggs" 315 | @-rm -fr "$(APP_ROOT)/develop-eggs" 316 | @-rm -fr "$(APP_ROOT)/$(APP_NAME).egg-info" 317 | @-rm -fr "$(APP_ROOT)/parts" 318 | 319 | .PHONY: clean-cache 320 | clean-cache: ## remove caches such as DOWNLOAD_CACHE 321 | @echo "Removing caches..." 322 | @-rm -fr "$(APP_ROOT)/.pytest_cache" 323 | @-rm -fr "$(DOWNLOAD_CACHE)" 324 | 325 | .PHONY: clean-docs 326 | clean-docs: clean-docs-dirs ## remove documentation artifacts 327 | @echo "Removing documentation build files..." 328 | @$(MAKE) -C "$(APP_ROOT)/docs" clean || true 329 | 330 | # extensive cleanup is possible only using sphinx-build 331 | # allow minimal cleanup when it could not *yet* be installed (dev) 332 | .PHONY: clean-docs-dirs 333 | clean-docs-dirs: ## remove documentation artifacts (minimal) 334 | @echo "Removing documentation directories..." 335 | @-rm -fr "$(APP_ROOT)/docs/_build" 336 | @-rm -fr "$(APP_ROOT)/docs/build" 337 | @-rm -fr "$(APP_ROOT)/docs/source/autoapi" 338 | @-rm -fr "$(APP_ROOT)/docs/html" 339 | @-rm -fr "$(APP_ROOT)/docs/xml" 340 | 341 | .PHONY: clean-src 342 | clean-src: ## remove all *.pyc files 343 | @echo "Removing python artifacts..." 344 | @-find "$(APP_ROOT)" -type f -name "*.pyc" -exec rm {} \; 345 | @-rm -rf ./build 346 | @-rm -rf ./src 347 | 348 | .PHONY: clean-test 349 | clean-test: ## remove files created by tests and coverage analysis 350 | @echo "Removing test/coverage/report files..." 351 | @-rm -f "$(APP_ROOT)/.coverage" 352 | @-rm -f "$(APP_ROOT)/coverage.*" 353 | @-rm -fr "$(APP_ROOT)/coverage" 354 | @-rm -fr "$(REPORTS_DIR)/coverage" 355 | @-rm -fr "$(REPORTS_DIR)/test-*.xml" 356 | 357 | .PHONY: clean-reports 358 | clean-reports: ## remove report files generated by code checks 359 | @-rm -fr "$(REPORTS_DIR)" 360 | 361 | .PHONY: clean-dist 362 | clean-dist: clean ## remove *all* files that are not controlled by 'git' except *.bak and makefile configuration 363 | @echo "Cleaning distribution..." 364 | @git diff --quiet HEAD || echo "There are uncommitted changes! Not doing 'git clean'..." 365 | @-git clean -dfx -e *.bak -e Makefile.config 366 | 367 | ## -- Testing targets ----------------------------------------------------------------------------------------------- ## 368 | ## -- [variants '-only' without '-only' suffix are also available with pre-install setup] 369 | 370 | # -v: list of test names with PASS/FAIL/SKIP/ERROR/etc. next to it 371 | # -vv: extended collection of stdout/stderr on top of test results 372 | TEST_VERBOSITY ?= -v 373 | override TEST_VERBOSE_FLAG := $(shell echo $(TEST_VERBOSITY) | tr ' ' '\n' | grep -E "^\-v+" || echo "") 374 | override TEST_VERBOSE_CAPTURE := $(shell \ 375 | test $$(echo "$(TEST_VERBOSE_FLAG)" | tr -cd 'v' | wc -c) -gt 1 && echo 1 || echo 0 \ 376 | ) 377 | ifeq ($(filter $(TEST_VERBOSITY),"--capture"),) 378 | ifeq ($(TEST_VERBOSE_CAPTURE),1) 379 | TEST_VERBOSITY := $(TEST_VERBOSITY) --capture tee-sys 380 | endif 381 | endif 382 | 383 | # autogen tests variants with pre-install of dependencies using the '-only' target references 384 | TESTS := unit spec coverage 385 | TESTS := $(addprefix test-, $(TESTS)) 386 | 387 | $(TESTS): test-%: install-dev test-%-only 388 | 389 | .PHONY: test 390 | test: clean-test test-all ## alias for 'test-all' target 391 | 392 | .PHONY: test-all 393 | test-all: install-dev test-only ## run all tests (including long running tests) 394 | 395 | .PHONY: test-only 396 | test-only: mkdir-reports ## run all tests but without prior validation of installed dependencies 397 | @echo "Running all tests (including slow and online tests)..." 398 | @bash -c '$(CONDA_CMD) pytest tests $(TEST_VERBOSITY) \ 399 | --junitxml "$(REPORTS_DIR)/test-results.xml"' 400 | 401 | .PHONY: test-unit-only 402 | test-unit-only: mkdir-reports ## run unit tests (skip long running and online tests) 403 | @echo "Running unit tests (skip slow and online tests)..." 404 | @bash -c '$(CONDA_CMD) pytest tests $(TEST_VERBOSITY) \ 405 | -m "not slow and not online and not functional" --junitxml "$(REPORTS_DIR)/test-results.xml"' 406 | 407 | .PHONY: test-spec-only 408 | test-spec-only: mkdir-reports ## run tests with custom specification (pytest format) [make SPEC='' test-spec] 409 | @echo "Running custom tests from input specification..." 410 | @[ "${SPEC}" ] || ( echo ">> 'SPEC' is not set"; exit 1 ) 411 | @bash -c '$(CONDA_CMD) pytest tests $(TEST_VERBOSITY) \ 412 | -k "${SPEC}" --junitxml "$(REPORTS_DIR)/test-results.xml"' 413 | 414 | .PHONY: test-coverage-only 415 | test-coverage-only: mkdir-reports ## run all tests using coverage analysis 416 | @echo "Running coverage analysis..." 417 | @bash -c '$(CONDA_CMD) coverage run --rcfile="$(APP_ROOT)/setup.cfg" "$$(which pytest)" "$(APP_ROOT)/tests" || true' 418 | @bash -c '$(CONDA_CMD) coverage xml --rcfile="$(APP_ROOT)/setup.cfg" -i -o "$(REPORTS_DIR)/coverage.xml"' 419 | @bash -c '$(CONDA_CMD) coverage report --rcfile="$(APP_ROOT)/setup.cfg" -i -m' 420 | @bash -c '$(CONDA_CMD) coverage html --rcfile="$(APP_ROOT)/setup.cfg" -d "$(REPORTS_DIR)/coverage"' 421 | 422 | .PHONY: coverage 423 | coverage: test-coverage ## alias to run test with coverage analysis 424 | 425 | ## -- Static code check targets ------------------------------------------------------------------------------------- ## 426 | ## -- [variants '-only' without '-only' suffix are also available with pre-install setup] 427 | 428 | # autogen check variants with pre-install of dependencies using the '-only' target references 429 | CHECKS := pep8 lint security security-code security-deps doc8 docf fstring docstring links imports 430 | CHECKS := $(addprefix check-, $(CHECKS)) 431 | 432 | # items that should not install python dev packages should be added here instead 433 | # they must provide their own target/only + with dependency install variants 434 | CHECKS_NO_PY := md 435 | CHECKS_NO_PY := $(addprefix check-, $(CHECKS_NO_PY)) 436 | CHECKS_ALL := $(CHECKS) $(CHECKS_NO_PY) 437 | 438 | $(CHECKS): check-%: install-dev check-%-only 439 | 440 | .PHONY: mkdir-reports 441 | mkdir-reports: 442 | @mkdir -p "$(REPORTS_DIR)" 443 | 444 | .PHONY: check 445 | check: check-all ## alias for 'check-all' target 446 | 447 | .PHONY: check-only 448 | check-only: $(addsuffix -only, $(CHECKS_ALL)) 449 | 450 | .PHONY: check-all 451 | check-all: install-dev $(CHECKS_ALL) ## check all code linters 452 | 453 | .PHONY: check-pep8-only 454 | check-pep8-only: mkdir-reports ## check for PEP8 code style issues 455 | @echo "Running PEP8 code style checks..." 456 | @-rm -fr "$(REPORTS_DIR)/check-pep8.txt" 457 | @bash -c '$(CONDA_CMD) \ 458 | flake8 --config="$(APP_ROOT)/setup.cfg" --output-file="$(REPORTS_DIR)/check-pep8.txt" --tee' 459 | 460 | .PHONY: check-lint-only 461 | check-lint-only: mkdir-reports ## check linting of code style 462 | @echo "Running linting code style checks..." 463 | @-rm -fr "$(REPORTS_DIR)/check-lint.txt" 464 | @bash -c '$(CONDA_CMD) \ 465 | pylint \ 466 | --load-plugins pylint_quotes \ 467 | --rcfile="$(APP_ROOT)/.pylintrc" \ 468 | --reports y \ 469 | "$(APP_ROOT)/$(APP_PKG)" "$(APP_ROOT)/tests" \ 470 | 1> >(tee "$(REPORTS_DIR)/check-lint.txt")' 471 | 472 | .PHONY: check-security-only 473 | check-security-only: check-security-code-only check-security-deps-only ## run security checks 474 | 475 | # FIXME: safety ignore file (https://github.com/pyupio/safety/issues/351) 476 | # ignored codes: 477 | # 45185: pylint<2.13.0: unrelated doc extension (https://github.com/PyCQA/pylint/issues/5322) 478 | SAFETY_IGNORE := 45185 479 | SAFETY_IGNORE := $(addprefix "-i ",$(SAFETY_IGNORE)) 480 | 481 | .PHONY: check-security-deps-only 482 | check-security-deps-only: mkdir-reports ## run security checks on package dependencies 483 | @echo "Running security checks of dependencies..." 484 | @-rm -fr "$(REPORTS_DIR)/check-security-deps.txt" 485 | @bash -c '$(CONDA_CMD) \ 486 | safety check \ 487 | --full-report \ 488 | -r "$(APP_ROOT)/requirements.txt" \ 489 | -r "$(APP_ROOT)/requirements-dev.txt" \ 490 | -r "$(APP_ROOT)/requirements-sys.txt" \ 491 | $(SAFETY_IGNORE) \ 492 | 1> >(tee "$(REPORTS_DIR)/check-security-deps.txt")' 493 | 494 | # FIXME: bandit excludes not working (https://github.com/PyCQA/bandit/issues/657), clean-src beforehand to avoid error 495 | .PHONY: check-security-code-only 496 | check-security-code-only: mkdir-reports clean-src ## run security checks on source code 497 | @echo "Running security code checks..." 498 | @-rm -fr "$(REPORTS_DIR)/check-security-code.txt" 499 | @bash -c '$(CONDA_CMD) \ 500 | bandit -v --ini "$(APP_ROOT)/setup.cfg" -r \ 501 | 1> >(tee "$(REPORTS_DIR)/check-security-code.txt")' 502 | 503 | .PHONY: check-doc8-only 504 | check-doc8-only: mkdir-reports ## check documentation RST styles and linting 505 | @echo "Running doc8 doc style checks..." 506 | @-rm -fr "$(REPORTS_DIR)/check-doc8.txt" 507 | @bash -c '$(CONDA_CMD) \ 508 | doc8 "$(APP_ROOT)/docs" \ 509 | 1> >(tee "$(REPORTS_DIR)/check-doc8.txt")' 510 | 511 | .PHONY: check-docf-only 512 | check-docf-only: mkdir-reports ## run PEP8 code documentation format checks 513 | @echo "Checking PEP8 doc formatting problems..." 514 | @-rm -fr "$(REPORTS_DIR)/check-docf.txt" 515 | @bash -c '$(CONDA_CMD) \ 516 | docformatter --check --recursive --config "$(APP_ROOT)/setup.cfg" "$(APP_ROOT)" \ 517 | 1>&2 2> >(tee "$(REPORTS_DIR)/check-docf.txt")' 518 | 519 | # FIXME: no configuration file support 520 | define FLYNT_FLAGS 521 | --line-length 120 \ 522 | --verbose 523 | endef 524 | ifeq ($(shell test $(PYTHON_VERSION_MAJOR) -eq 3 && test $(PYTHON_VERSION_MINOR) -ge 8; echo $$?),0) 525 | FLYNT_FLAGS := $(FLYNT_FLAGS) --transform-concats 526 | endif 527 | 528 | .PHONY: check-fstring-only 529 | check-fstring-only: mkdir-reports ## check f-string format definitions 530 | @echo "Running code f-string formats substitutions..." 531 | @-rm -f "$(REPORTS_DIR)/check-fstring.txt" 532 | @bash -c '$(CONDA_CMD) \ 533 | flynt --dry-run --fail-on-change $(FLYNT_FLAGS) "$(APP_ROOT)" \ 534 | 1> >(tee "$(REPORTS_DIR)/check-fstring.txt")' 535 | 536 | .PHONY: check-docstring-only 537 | check-docstring-only: mkdir-reports ## check code docstring style and linting 538 | @echo "Running docstring checks..." 539 | @-rm -fr "$(REPORTS_DIR)/check-docstring.txt" 540 | @bash -c '$(CONDA_CMD) \ 541 | pydocstyle --explain --config "$(APP_ROOT)/setup.cfg" "$(APP_ROOT)" \ 542 | 1> >(tee "$(REPORTS_DIR)/check-docstring.txt")' 543 | 544 | .PHONY: check-links-only 545 | check-links-only: ## check all external links in documentation for integrity 546 | @echo "Running link checks on docs..." 547 | @bash -c '$(CONDA_CMD) $(MAKE) -C "$(APP_ROOT)/docs" linkcheck' || true 548 | 549 | .PHONY: check-imports-only 550 | check-imports-only: mkdir-reports ## check imports ordering and styles 551 | @echo "Running import checks..." 552 | @-rm -fr "$(REPORTS_DIR)/check-imports.txt" 553 | @bash -c '$(CONDA_CMD) \ 554 | isort --check-only --diff --recursive $(APP_ROOT) \ 555 | 1> >(tee "$(REPORTS_DIR)/check-imports.txt")' 556 | 557 | # must pass 2 search paths because '/.' are somehow not correctly detected with only the top-level 558 | .PHONY: check-md-only 559 | check-md-only: mkdir-reports ## check Markdown linting 560 | @echo "Running Markdown style checks..." 561 | @npx --no-install remark \ 562 | --inspect --frail \ 563 | --silently-ignore \ 564 | --stdout --color \ 565 | --rc-path "$(APP_ROOT)/.remarkrc" \ 566 | --ignore-path "$(APP_ROOT)/.remarkignore" \ 567 | "$(APP_ROOT)" "$(APP_ROOT)/.*/" \ 568 | > "$(REPORTS_DIR)/check-md.txt" 569 | 570 | .PHONY: check-md 571 | check-md: install-npm-remarklint check-md-only ## check Markdown linting after dependency installation 572 | 573 | # autogen fix variants with pre-install of dependencies using the '-only' target references 574 | FIXES := imports lint docf fstring 575 | FIXES := $(addprefix fix-, $(FIXES)) 576 | # items that should not install python dev packages should be added here instead 577 | # they must provide their own target/only + with dependency install variants 578 | FIXES_NO_PY := md 579 | FIXES_NO_PY := $(addprefix fix-, $(FIXES_NO_PY)) 580 | FIXES_ALL := $(FIXES) $(FIXES_NO_PY) 581 | 582 | $(FIXES): fix-%: install-dev fix-%-only 583 | 584 | .PHONY: fix 585 | fix: fix-all ## alias for 'fix-all' target 586 | 587 | .PHONY: fix-only 588 | fix-only: $(addsuffix -only, $(FIXES)) ## run all automatic fixes without development dependencies pre-install 589 | 590 | .PHONY: fix-all 591 | fix-all: install-dev $(FIXES_ALL) ## fix all code check problems automatically after install of dependencies 592 | 593 | .PHONY: fix-imports-only 594 | fix-imports-only: mkdir-reports ## apply import code checks corrections 595 | @echo "Fixing flagged import checks..." 596 | @-rm -fr "$(REPORTS_DIR)/fixed-imports.txt" 597 | @bash -c '$(CONDA_CMD) \ 598 | isort --recursive $(APP_ROOT) \ 599 | 1> >(tee "$(REPORTS_DIR)/fixed-imports.txt")' 600 | 601 | # FIXME: https://github.com/PyCQA/pycodestyle/issues/996 602 | # Tool "pycodestyle" doesn't respect "# noqa: E241" locally, but "flake8" and other tools do. 603 | # Because "autopep8" uses "pycodestyle", it is impossible to disable locally extra spaces (as in tests to align values). 604 | # Override the codes here from "setup.cfg" because "autopep8" also uses the "flake8" config, and we want to preserve 605 | # global detection of those errors (typos, bad indents), unless explicitly added and excluded for readability purposes. 606 | # WARNING: this will cause inconsistencies between what 'check-lint' detects and what 'fix-lint' can actually fix 607 | _DEFAULT_SETUP_ERROR := E126,E226,E402,F401,W503,W504 608 | _EXTRA_SETUP_ERROR := E241,E731 609 | 610 | .PHONY: fix-lint-only 611 | fix-lint-only: mkdir-reports ## fix some PEP8 code style problems automatically 612 | @echo "Fixing PEP8 code style problems..." 613 | @-rm -fr "$(REPORTS_DIR)/fixed-lint.txt" 614 | @bash -c '$(CONDA_CMD) \ 615 | autopep8 \ 616 | --global-config "$(APP_ROOT)/setup.cfg" \ 617 | --ignore "$(_DEFAULT_SETUP_ERROR),$(_EXTRA_SETUP_ERROR)" \ 618 | -v -j 0 -i -r $(APP_ROOT) \ 619 | 1> >(tee "$(REPORTS_DIR)/fixed-lint.txt")' 620 | 621 | .PHONY: fix-docf-only 622 | fix-docf-only: mkdir-reports ## fix some PEP8 code documentation style problems automatically 623 | @echo "Fixing PEP8 code documentation problems..." 624 | @-rm -fr "$(REPORTS_DIR)/fixed-docf.txt" 625 | @bash -c '$(CONDA_CMD) \ 626 | docformatter --in-place --recursive --config "$(APP_ROOT)/setup.cfg" "$(APP_ROOT)" \ 627 | 1> >(tee "$(REPORTS_DIR)/fixed-docf.txt")' 628 | 629 | .PHONY: fix-fstring-only 630 | fix-fstring-only: mkdir-reports 631 | @echo "Fixing code string formats substitutions to f-string definitions..." 632 | @-rm -f "$(REPORTS_DIR)/fixed-fstring.txt" 633 | @bash -c '$(CONDA_CMD) \ 634 | flynt $(FLYNT_FLAGS) "$(APP_ROOT)" \ 635 | 1> >(tee "$(REPORTS_DIR)/fixed-fstring.txt")' 636 | 637 | # must pass 2 search paths because '/.' are somehow not correctly detected with only the top-level 638 | .PHONY: fix-md-only 639 | fix-md-only: mkdir-reports ## fix Markdown linting problems automatically 640 | @echo "Running Markdown style checks..." 641 | @npx --no-install remark \ 642 | --output --frail \ 643 | --silently-ignore \ 644 | --rc-path "$(APP_ROOT)/.remarkrc" \ 645 | --ignore-path "$(APP_ROOT)/.remarkignore" \ 646 | "$(APP_ROOT)" "$(APP_ROOT)/.*/" \ 647 | 2>&1 | tee "$(REPORTS_DIR)/fixed-md.txt" 648 | 649 | .PHONY: fix-md 650 | fix-md: install-npm-remarklint fix-md-only ## fix Markdown linting problems after dependency installation 651 | 652 | ## -- Documentation targets ----------------------------------------------------------------------------------------- ## 653 | 654 | .PHONY: docs-build 655 | docs-build: ## generate HTML documentation with Sphinx 656 | @echo "Generating docs with Sphinx..." 657 | @bash -c '$(CONDA_CMD) $(MAKE) -C "$(APP_ROOT)/docs" html' 658 | @-echo "Documentation available: file://$(APP_ROOT)/docs/build/html/index.html" 659 | 660 | .PHONY: docs-only 661 | docs-only: docs-build ## generate HTML documentation with Sphinx (alias) 662 | 663 | .PHONY: docs 664 | docs: install-doc clean-docs docs-only ## generate HTML documentation with Sphinx after dependencies installation 665 | 666 | ## -- Versioning targets -------------------------------------------------------------------------------------------- ## 667 | 668 | # Bumpversion 'dry' config 669 | # if 'dry' is specified as target, any bumpversion call using 'BUMP_XARGS' will not apply changes 670 | BUMP_XARGS ?= --verbose --allow-dirty 671 | ifeq ($(filter dry, $(MAKECMDGOALS)), dry) 672 | BUMP_XARGS := $(BUMP_XARGS) --dry-run --verbose --list 673 | endif 674 | .PHONY: dry 675 | dry: setup.cfg ## run 'bump' target without applying changes (dry-run) [make VERSION= bump dry] 676 | @-echo > /dev/null 677 | 678 | .PHONY: bump 679 | bump: ## bump version using VERSION specified as user input [make VERSION= bump] 680 | @-echo "Updating package version ..." 681 | @[ "${VERSION}" ] || ( echo ">> 'VERSION' is not set"; exit 1 ) 682 | @-bash -c '$(CONDA_CMD) bump2version $(BUMP_XARGS) --new-version "${VERSION}" patch;' 683 | 684 | # Reapply config if overrides were defined. 685 | # Ensure overrides take precedence over targets and auto-resolution logic of variables. 686 | -include Makefile.config 687 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ML AOI Extension 2 | 3 | 4 | 5 | 6 | - **Title:** ML AOI 7 | - **Identifier:** [https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json](https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json) 8 | - **Field Name Prefix:** ml-aoi 9 | - **Scope:** Collection, Item, Asset, Links 10 | - **Extension [Maturity Classification](https://github.com/radiantearth/stac-spec/tree/master/extensions/README.md#extension-maturity):** Proposal 11 | - **Owner**: @fmigneault @echeipesh @kbgg @duckontheweb 12 | 13 | 14 | This document explains the ML-AOI Extension to the 15 | [SpatioTemporal Asset Catalog](https://github.com/radiantearth/stac-spec) (STAC) specification. 16 | 17 | An Item and Collection extension to provide labeled training data for machine learning models. 18 | This extension relies on but is distinct from the existing [`label`][stac-label] extension. 19 | STAC items using the [`label`][stac-label] extension link label assets with the source 20 | imagery for which they are valid, often as result of human labelling effort. 21 | By contrast STAC items using `ml-aoi` extension link label assets with raster items for each specific 22 | machine learning model that is being trained. 23 | 24 | In addition to linking labels with feature items the `ml-aoi` extension addresses some of the 25 | common configurations for ML workflows. 26 | The use of this extension is intended to make the model training process reproducible as well 27 | as providing model provenance once the model is trained. 28 | 29 | ## Item Properties and Collection Fields 30 | 31 | | Field Name | Type | Description | 32 | | -------------- | ------ | ---------------------------------------------------------- | 33 | | `ml-aoi:split` | string | Assigns item to one of `train`, `test`, or `validate` sets | 34 | 35 | ### Additional Field Information 36 | 37 | #### ml-aoi:split 38 | 39 | This field is optional. 40 | If not provided, it is expected that the split property will be added later before consuming the items. 41 | 42 | #### bbox and geometry 43 | 44 | - `ml-aoi` Multiple items may reference the same label and image item by scoping the `bbox` and `geometry` fields. 45 | - `ml-aoi` Items `bbox` field may overlap when they belong to different `ml-aoi:split` set. 46 | - `ml-aoi` Items in the same Collection should never have overlapping `geometry` fields. 47 | 48 | ## Links 49 | 50 | `ml-aoi` Item must link to both label and raster STAC items valid for its area of interest. 51 | These Link objects should set `rel` field to `derived_from` for both label and feature items. 52 | 53 | `ml-aoi` Item should contain enough metadata to make it consumable without the need for following the label 54 | and feature link item links. In reality this may not be practical because the use-case may not be fully known 55 | at the time the Item is generated. Therefore, it is critical that source label and feature items are linked to 56 | provide the future consumer the option to collect additional metadata from them. 57 | 58 | | Field Name | Type | Name | Description | 59 | | ------------- | ------ | ---- | -------------------- | 60 | | `ml-aoi:role` | string | Role | `label` or `feature` | 61 | 62 | ### Labels 63 | 64 | An `ml-aoi` Item must link to exactly one STAC item that is using `label` extension. 65 | Label links should provide `ml-aoi:role` field set to `label` value. 66 | 67 | ### Features 68 | 69 | An `ml-aoi` Item must link to at least one raster STAC item. 70 | Feature links should provide `ml-aoi:role` field set to `feature` value. 71 | 72 | Linked feature STAC items may use `eo` but that is not required. 73 | It is up to the consumer of `ml-aoi` Items to decide how to use the linked feature rasters. 74 | 75 | ## Assets 76 | 77 | Item should directly include assets for label and feature rasters. 78 | 79 | 80 | 81 | | Field Name | Type | Name | Description | 82 | | -------------------------- | ------ | ----------------- | ------------------------------------------------------------ | 83 | | `ml-aoi:role` | string | Role | `label` or `feature` | 84 | | `ml-aoi:reference-grid` | bool | Reference Grid | This raster provides reference pixel grid for model training | 85 | | `ml-aoi:resampling-method` | string | Resampling Method | Resampling method for non-reference-grid feature rasters | 86 | 87 | 88 | 89 | Resampling method should be one of the values [supported by gdalwarp](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r) 90 | 91 | ### Labels 92 | 93 | Assets for the label item can be copied directly from the label item with their asset name preserved. 94 | Label assets should provide `ml-aoi:role` field set to `label` value. 95 | 96 | ### Features 97 | 98 | Assets for the raster item can be copied directly from the label item with their asset name preserved. 99 | Feature assets should provide `ml-aoi:role` field set to `feature` value. 100 | 101 | When multiple raster features are included their resolutions and pixel grids are not likely to align. 102 | One raster may specify `ml-aoi:reference-grid` field set to `true` to indicate that all other features 103 | should be resampled to match its pixel grid during model training. 104 | Other raster assets should be resampled to the reference pixel grid. 105 | 106 | ## Collection 107 | 108 | All `ml-aoi` Items should belong to a Collection that designates a specific model training input. 109 | There is one-to-one mapping between a single ml-aoi collection and a machine-learning model. 110 | 111 | ### Collection fields 112 | 113 | The consumer of `ml-aoi` catalog needs to understand the available label classes and features without crawling 114 | the full catalog. 115 | When member Items include multiple feature rasters it is possible that not all of them will overlap every AOI. 116 | 117 | ## Contributing 118 | 119 | All contributions are subject to the 120 | [STAC Specification Code of Conduct](https://github.com/radiantearth/stac-spec/blob/master/CODE_OF_CONDUCT.md). 121 | For contributions, please follow the 122 | [STAC specification contributing guide](https://github.com/radiantearth/stac-spec/blob/master/CONTRIBUTING.md) 123 | instructions for running tests are copied here for convenience. 124 | 125 | ### Running tests 126 | 127 | The same checks that run as checks on PRs are part of the repository and can be run locally to verify 128 | that changes are valid. 129 | To run tests locally, you'll need `npm`, which is a standard part of any 130 | [node.js installation](https://nodejs.org/en/download/). 131 | 132 | First you'll need to install everything with npm once. Just navigate to the root of this repository and on 133 | your command line run: 134 | 135 | ```bash 136 | npm install 137 | ``` 138 | 139 | Then to check Markdown formatting and test the examples against the JSON schema, you can run: 140 | 141 | ```bash 142 | npm test 143 | ``` 144 | 145 | This will spit out the same texts that you see online, and you can then go and fix your markdown or examples. 146 | 147 | If the tests reveal formatting problems with the examples, you can fix them with: 148 | 149 | ```bash 150 | npm run format-examples 151 | ``` 152 | 153 | ## Design Decisions 154 | 155 | Central choices and rationale behind them is outlined in the ADR format: 156 | 157 | | ID | ADR | 158 | | ---- | ----------------------------------------------------------------------- | 159 | | 0002 | [Use Case](docs/0002-use-case-definition.md) | 160 | | 0003 | [Test/Train/Validation Split](docs/0003-test-train-validation-split.md) | 161 | | 0004 | [Sourcing Multiple Label Items](docs/0004-multiple-label-items.md) | 162 | 163 | [stac-label]: https://github.com/stac-extensions/label 164 | -------------------------------------------------------------------------------- /docs/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | Date: 2020-08-08 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | ## Consequences 18 | 19 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 20 | -------------------------------------------------------------------------------- /docs/0002-use-case-definition.md: -------------------------------------------------------------------------------- 1 | # 2. Us- case definition 2 | 3 | Date: 2020-08-10 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We define the initial use case for `ml-aoi` spec that exposes assumptions and reasoning for specific layout choices: 12 | providing training data source for Raster Vision model training process. 13 | 14 | `ml-aoi` STAC Items represent a reified relation between feature rasters and ground-truth label in a machine learning 15 | training dataset. 16 | Each `ml-aoi` Item roughly correspond to a "scene" or a training example. 17 | 18 | ### Justification for new extension 19 | 20 | Current known STAC extensions are not suitable for this purpose. The closest match is the STAC `label` extension. 21 | `label` extension provides a way to define either vector or raster labels over area. 22 | However, it does not provide a mechanism to link those labels with feature images; 23 | links with `rel` type `source` point to imagery from which labels were derived. 24 | Sometimes this imagery will be used as feature input for model training, but not always. 25 | The concept of source label imagery and input feature imagery are semantically distinct. 26 | For instance, it is possible to apply a single source of ground-truth building labels to train a model on either 27 | Landsat or Sentinel-2 scenes. 28 | 29 | ### Catalog Lifetime 30 | 31 | `ml-aoi` Item links to both raster STAC item and label STAC item. 32 | In this relationship the source raster and label items are static and long-lived, being used by several `ml-aoi` 33 | catalogs. By contrast `ml-aoi` catalog is somewhat ephemeral, it captures the training set in order to provide model 34 | reproducibility and provenance. 35 | There can be any number of `ml-aoi` catalogs linking to the same raster and label items, while varying selection, 36 | training/testing/validation split and class configuration. 37 | 38 | ## Decision 39 | 40 | We will adopt the use and development of `ml-aoi` extension in future machine-learning projects. 41 | 42 | ## Consequences 43 | 44 | We will no longer attempt to use `label` extension as a sole source of training data for ML models. 45 | We will continue development of tools to both produce and consume `ml-aoi` extension catalogs. 46 | -------------------------------------------------------------------------------- /docs/0003-test-train-validation-split.md: -------------------------------------------------------------------------------- 1 | # 3. Test-train-validation split 2 | 3 | Date: 2020-08-10 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | During model training, it is important to have a consistent split between training, testing and validation data. 12 | 13 | - Training subset is used to tune model weights. 14 | - Test subset is used to monitor training progress and hyperparameter turning. 15 | - Validation subset is used to judge overall model performance. 16 | 17 | Best practices dictate that it is critical that these datasets do not overlap. 18 | The which items are selected for this split will affect model performance and should be captured in 19 | the `ml-aoi` catalog. 20 | 21 | In context of a STAC catalog there are multiple ways to express the data split. 22 | This ADR explores available options and their consequences. 23 | 24 | ### Split by Collection 25 | 26 | Split could be generated by generating a separate collection for each set. This is a flexible approach. 27 | However, the grouping of these collections into one cohesive training set would have to be done by convention, 28 | for instance by prefix on collection `id`. 29 | Additionally, these collections could not be easily visualized together. 30 | Most (all?) existing STAC viewers are focused on browsing or viewing one collection at a time. 31 | 32 | Additionally, the convention of how to associate training with testing with validation set would have to be propagated 33 | into downstream tooling. 34 | Further it would be easy to include a single item in both training and testing set without realizing it. 35 | This is not a good choice for these reasons. 36 | 37 | ### Split by Link property 38 | 39 | The top-most `ml-aoi` collection has to link to each item or child catalogs. 40 | These links could have additional property that designates the split. 41 | This approach keeps all the items with in the same collection, which is good. 42 | 43 | However, when ingested into STAC API this link property is often lost and is not easily queried. 44 | Thus, the split set membership would not be visible to through STAC API, which is bad. 45 | This is not a good choice for that reason. 46 | 47 | ### Split by Item property 48 | 49 | Each item could have an extension specific property (ex: `ml-aoi:split`) that designates set membership. 50 | This approach addresses the short-comings of the previous methods. 51 | 52 | This property can be easily searched for after item is ingested into STAC API. 53 | Following this method it is not possible to include a single item in multiple sets. 54 | Collection can be viewed by tools that do not understand `ml-aoi` extension. 55 | 56 | ## Decision 57 | 58 | Test, Train, Validation split should be handled by `ml-aoi:split` Item property. 59 | Keeping the all items, regardless of the role, grouped in a single collection provides the best integration 60 | with other STAC tools. 61 | Expected use case is visual inspection of items on a single map with role membership used to color the 62 | footprint polygons. 63 | 64 | ## Consequences 65 | 66 | Future `ml-aoi` catalogs should include `ml-aoi:split` property. 67 | -------------------------------------------------------------------------------- /docs/0004-multiple-label-items.md: -------------------------------------------------------------------------------- 1 | # 4. Multiple label items 2 | 3 | Date: 2020-08-11 4 | 5 | ## Status 6 | 7 | Proposed 8 | 9 | ## Context 10 | 11 | Should each `ml-aoi` Item be able to bring in multiple labels? 12 | This would be a useful feature for training multi-class classifiers. 13 | One can imagine having a label STAC item for buildings and separate STAC item for fields. 14 | STAC Items Links object is an array, so many label items could be linked to from a single `ml-aoi` STAC Item. 15 | 16 | ### Limiting to single label link 17 | 18 | Limiting to single label link however is appealing because the label item metadata could be copied over to `ml-aoi` 19 | Item. This would remove the need to follow the link for the label item during processing. 20 | In practice this would make each `ml-aoi` Item also a `label` Item, allowing for its re-use by tooling that 21 | understands `label`. 22 | 23 | If multi-class label dataset would be required there would have to be a mechanical pre-processing step of combining 24 | existing labels into a single STAC `label` item. This could mean either union of GeoJSON FeatureCollections per item or 25 | a configuration of a more complex STAC `label` Item that links to multiple label assets. 26 | 27 | ### Allowing multiple labels 28 | 29 | The main appeal of consuming multi-label `ml-aoi` items is that it would allow referencing multiple label sources, 30 | some which could be external, without the need for pre-processing and thus minimizing data duplication. 31 | 32 | If multiple labels were to be allowed the `ml-aoi` the pre-processing step above would be pushed into `ml-aoi` consumer. 33 | The consumer would need appropriate metadata in order to decipher how the label structure. 34 | This would require either crawling the full catalog or some kind of meta-label structure that combines the metadata 35 | from all the included labels into a single structure that could be interpreted by the consumer. 36 | 37 | ## Decision 38 | 39 | `ml-aoi` Items should be limited to linking to only a single label item. 40 | Requiring the consumer to interpret multiple label items pushed unreasonable complexity on the user. 41 | Additionally, combining labels likely requires series of processing and validation steps. 42 | Each one of those would likely require judgment calls and exceptions. 43 | For instance when combining building and fields label datasets the user should check that no building and field 44 | polygons overlap. 45 | 46 | It is not realistic to expect all possible requirements of that process to be expressed by a simple metadata structure. 47 | Therefore, it is better to explicitly require the label combination as a separate process done by the user. 48 | The resulting label catalog can capture that design and iteration required for that process anyway. 49 | 50 | ## Consequences 51 | 52 | `ml-aoi` Items can copy all `label` extension properties from the `label` Item. 53 | In effect `ml-aoi` Items extends `label` item by adding links to feature imagery. 54 | This formulation lines up with original problem statement for `ml-aoi` extension. 55 | -------------------------------------------------------------------------------- /examples/collection_EuroSAT-subset-train.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [ 4 | "https://stac-extensions.github.io/eo/v1.1.0/schema.json", 5 | "https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json", 6 | "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", 7 | "https://stac-extensions.github.io/stats/v0.2.0/schema.json", 8 | "https://stac-extensions.github.io/version/v1.0.0/schema.json", 9 | "https://stac-extensions.github.io/view/v1.0.0/schema.json" 10 | ], 11 | "type": "Collection", 12 | "id": "EuroSAT-subset-train", 13 | "title": "EuroSAT subset train", 14 | "description": "EuroSAT dataset with labeled annotations for land-cover classification and associated imagery. This collection represents the samples part of the train split set for training machine learning algorithms.", 15 | "version": "0.5.0", 16 | "experimental": true, 17 | "license": "MIT", 18 | "extent": { 19 | "spatial": { 20 | "bbox": [ 21 | [ 22 | -7.882190080512502, 23 | 37.13739173208318, 24 | 27.911651652899923, 25 | 58.21798141355221 26 | ] 27 | ] 28 | }, 29 | "temporal": { 30 | "interval": [ 31 | [ 32 | "2015-06-27T10:25:31.456Z", 33 | "2017-06-14T00:00:00Z" 34 | ] 35 | ] 36 | } 37 | }, 38 | "summaries": { 39 | "sci:doi": [ 40 | "10.1109/JSTARS.2019.2918242" 41 | ], 42 | "sci:citation": [ 43 | "Eurosat: A novel dataset and deep learning benchmark for land use and land cover classification. Patrick Helber, Benjamin Bischke, Andreas Dengel, Damian Borth. IEEE Journal of Selected Topics in Applied Earth Observations and Remote Sensing, 2019." 44 | ], 45 | "sci:publications": [ 46 | { 47 | "doi": "10.1109/IGARSS.2018.8519248", 48 | "citation": "Introducing EuroSAT: A Novel Dataset and Deep Learning Benchmark for Land Use and Land Cover Classification. Patrick Helber, Benjamin Bischke, Andreas Dengel. 2018 IEEE International Geoscience and Remote Sensing Symposium, 2018." 49 | } 50 | ], 51 | "ml-aoi:split": [ 52 | "train" 53 | ], 54 | "constellation": [ 55 | "sentinel-2" 56 | ], 57 | "instruments": [ 58 | "msi" 59 | ], 60 | "eo:bands": [ 61 | { 62 | "name": "B01", 63 | "common_name": "coastal", 64 | "center_wavelength": 0.4439, 65 | "full_width_half_max": 0.027 66 | }, 67 | { 68 | "name": "B02", 69 | "common_name": "blue", 70 | "center_wavelength": 0.4966, 71 | "full_width_half_max": 0.098 72 | }, 73 | { 74 | "name": "B03", 75 | "common_name": "green", 76 | "center_wavelength": 0.56, 77 | "full_width_half_max": 0.045 78 | }, 79 | { 80 | "name": "B04", 81 | "common_name": "red", 82 | "center_wavelength": 0.6645, 83 | "full_width_half_max": 0.038 84 | }, 85 | { 86 | "name": "B05", 87 | "center_wavelength": 0.7039, 88 | "full_width_half_max": 0.019, 89 | "common_name": "rededge" 90 | }, 91 | { 92 | "name": "B06", 93 | "center_wavelength": 0.7402, 94 | "full_width_half_max": 0.018, 95 | "common_name": "rededge" 96 | }, 97 | { 98 | "name": "B07", 99 | "center_wavelength": 0.7825, 100 | "full_width_half_max": 0.028, 101 | "common_name": "rededge" 102 | }, 103 | { 104 | "name": "B08", 105 | "common_name": "nir", 106 | "center_wavelength": 0.8351, 107 | "full_width_half_max": 0.145 108 | }, 109 | { 110 | "name": "B08A", 111 | "center_wavelength": 0.8648, 112 | "full_width_half_max": 0.033, 113 | "common_name": "nir08" 114 | }, 115 | { 116 | "name": "B09", 117 | "center_wavelength": 0.945, 118 | "full_width_half_max": 0.026, 119 | "common_name": "nir09" 120 | }, 121 | { 122 | "name": "B10", 123 | "common_name": "cirrus", 124 | "center_wavelength": 1.3735, 125 | "full_width_half_max": 0.075 126 | }, 127 | { 128 | "name": "B11", 129 | "common_name": "swir16", 130 | "center_wavelength": 1.6137, 131 | "full_width_half_max": 0.143 132 | }, 133 | { 134 | "name": "B12", 135 | "common_name": "swir22", 136 | "center_wavelength": 2.22024, 137 | "full_width_half_max": 0.242 138 | } 139 | ], 140 | "view:off_nadir": [ 141 | 0 142 | ], 143 | "gsd": [ 144 | 10 145 | ] 146 | }, 147 | "assets": { 148 | "source": { 149 | "href": "https://github.com/phelber/EuroSAT/", 150 | "type": "text/html", 151 | "roles": [ 152 | "data", 153 | "source", 154 | "scientific", 155 | "citation" 156 | ], 157 | "title": "GitHub repository", 158 | "description": "Source GitHub repository of the EuroSAT dataset.", 159 | "sci:doi": "10.1109/JSTARS.2019.2918242" 160 | }, 161 | "paper": { 162 | "href": "https://www.researchgate.net/publication/319463676", 163 | "type": "text/html", 164 | "roles": [ 165 | "paper", 166 | "scientific", 167 | "citation" 168 | ], 169 | "title": "Scientific Paper", 170 | "description": "ResearchGate page with embedded PDF of the scientific paper supporting the dataset.", 171 | "sci:doi": "10.1109/JSTARS.2019.2918242" 172 | }, 173 | "thumbnail": { 174 | "href": "https://raw.githubusercontent.com/phelber/EuroSAT/master/eurosat_overview_small.jpg", 175 | "type": "image/jpeg", 176 | "roles": [ 177 | "thumbnail", 178 | "overview" 179 | ], 180 | "description": "Preview of dataset samples.", 181 | "sci:doi": "10.1109/JSTARS.2019.2918242" 182 | }, 183 | "license": { 184 | "href": "https://raw.githubusercontent.com/phelber/EuroSAT/master/LICENSE", 185 | "type": "text/plain", 186 | "roles": [ 187 | "legal", 188 | "license" 189 | ], 190 | "title": "License", 191 | "description": "License contents associated to the EuroSAT dataset.", 192 | "sci:doi": "10.1109/JSTARS.2019.2918242" 193 | } 194 | }, 195 | "stats:items": { 196 | "count": 60 197 | }, 198 | "links": [ 199 | { 200 | "rel": "cite-as", 201 | "href": "https://arxiv.org/abs/1709.00029", 202 | "type": "text/html", 203 | "title": "EuroSAT: A Novel Dataset and Deep Learning Benchmark for Land Use and Land Cover Classification" 204 | }, 205 | { 206 | "rel": "license", 207 | "href": "https://raw.githubusercontent.com/phelber/EuroSAT/master/LICENSE", 208 | "type": "text/html", 209 | "title": "EuroSAT: A Novel Dataset and Deep Learning Benchmark for Land Use and Land Cover Classification" 210 | }, 211 | { 212 | "rel": "item", 213 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-0.json", 214 | "type": "application/geo+json" 215 | }, 216 | { 217 | "rel": "item", 218 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-1.json", 219 | "type": "application/geo+json" 220 | }, 221 | { 222 | "rel": "item", 223 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-2.json", 224 | "type": "application/geo+json" 225 | }, 226 | { 227 | "rel": "item", 228 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-3.json", 229 | "type": "application/geo+json" 230 | }, 231 | { 232 | "rel": "item", 233 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-4.json", 234 | "type": "application/geo+json" 235 | }, 236 | { 237 | "rel": "item", 238 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-5.json", 239 | "type": "application/geo+json" 240 | }, 241 | { 242 | "rel": "item", 243 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-6.json", 244 | "type": "application/geo+json" 245 | }, 246 | { 247 | "rel": "item", 248 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-7.json", 249 | "type": "application/geo+json" 250 | }, 251 | { 252 | "rel": "item", 253 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-8.json", 254 | "type": "application/geo+json" 255 | }, 256 | { 257 | "rel": "item", 258 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-9.json", 259 | "type": "application/geo+json" 260 | }, 261 | { 262 | "rel": "item", 263 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-10.json", 264 | "type": "application/geo+json" 265 | }, 266 | { 267 | "rel": "item", 268 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-11.json", 269 | "type": "application/geo+json" 270 | }, 271 | { 272 | "rel": "item", 273 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-12.json", 274 | "type": "application/geo+json" 275 | }, 276 | { 277 | "rel": "item", 278 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-13.json", 279 | "type": "application/geo+json" 280 | }, 281 | { 282 | "rel": "item", 283 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-14.json", 284 | "type": "application/geo+json" 285 | }, 286 | { 287 | "rel": "item", 288 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-15.json", 289 | "type": "application/geo+json" 290 | }, 291 | { 292 | "rel": "item", 293 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-16.json", 294 | "type": "application/geo+json" 295 | }, 296 | { 297 | "rel": "item", 298 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-17.json", 299 | "type": "application/geo+json" 300 | }, 301 | { 302 | "rel": "item", 303 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-18.json", 304 | "type": "application/geo+json" 305 | }, 306 | { 307 | "rel": "item", 308 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-19.json", 309 | "type": "application/geo+json" 310 | }, 311 | { 312 | "rel": "item", 313 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-20.json", 314 | "type": "application/geo+json" 315 | }, 316 | { 317 | "rel": "item", 318 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-21.json", 319 | "type": "application/geo+json" 320 | }, 321 | { 322 | "rel": "item", 323 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-22.json", 324 | "type": "application/geo+json" 325 | }, 326 | { 327 | "rel": "item", 328 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-23.json", 329 | "type": "application/geo+json" 330 | }, 331 | { 332 | "rel": "item", 333 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-24.json", 334 | "type": "application/geo+json" 335 | }, 336 | { 337 | "rel": "item", 338 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-25.json", 339 | "type": "application/geo+json" 340 | }, 341 | { 342 | "rel": "item", 343 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-26.json", 344 | "type": "application/geo+json" 345 | }, 346 | { 347 | "rel": "item", 348 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-27.json", 349 | "type": "application/geo+json" 350 | }, 351 | { 352 | "rel": "item", 353 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-28.json", 354 | "type": "application/geo+json" 355 | }, 356 | { 357 | "rel": "item", 358 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-29.json", 359 | "type": "application/geo+json" 360 | }, 361 | { 362 | "rel": "item", 363 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-30.json", 364 | "type": "application/geo+json" 365 | }, 366 | { 367 | "rel": "item", 368 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-31.json", 369 | "type": "application/geo+json" 370 | }, 371 | { 372 | "rel": "item", 373 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-32.json", 374 | "type": "application/geo+json" 375 | }, 376 | { 377 | "rel": "item", 378 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-33.json", 379 | "type": "application/geo+json" 380 | }, 381 | { 382 | "rel": "item", 383 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-34.json", 384 | "type": "application/geo+json" 385 | }, 386 | { 387 | "rel": "item", 388 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-35.json", 389 | "type": "application/geo+json" 390 | }, 391 | { 392 | "rel": "item", 393 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-36.json", 394 | "type": "application/geo+json" 395 | }, 396 | { 397 | "rel": "item", 398 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-37.json", 399 | "type": "application/geo+json" 400 | }, 401 | { 402 | "rel": "item", 403 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-38.json", 404 | "type": "application/geo+json" 405 | }, 406 | { 407 | "rel": "item", 408 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-39.json", 409 | "type": "application/geo+json" 410 | }, 411 | { 412 | "rel": "item", 413 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-40.json", 414 | "type": "application/geo+json" 415 | }, 416 | { 417 | "rel": "item", 418 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-41.json", 419 | "type": "application/geo+json" 420 | }, 421 | { 422 | "rel": "item", 423 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-42.json", 424 | "type": "application/geo+json" 425 | }, 426 | { 427 | "rel": "item", 428 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-43.json", 429 | "type": "application/geo+json" 430 | }, 431 | { 432 | "rel": "item", 433 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-44.json", 434 | "type": "application/geo+json" 435 | }, 436 | { 437 | "rel": "item", 438 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-45.json", 439 | "type": "application/geo+json" 440 | }, 441 | { 442 | "rel": "item", 443 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-46.json", 444 | "type": "application/geo+json" 445 | }, 446 | { 447 | "rel": "item", 448 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-47.json", 449 | "type": "application/geo+json" 450 | }, 451 | { 452 | "rel": "item", 453 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-48.json", 454 | "type": "application/geo+json" 455 | }, 456 | { 457 | "rel": "item", 458 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-49.json", 459 | "type": "application/geo+json" 460 | }, 461 | { 462 | "rel": "item", 463 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-50.json", 464 | "type": "application/geo+json" 465 | }, 466 | { 467 | "rel": "item", 468 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-51.json", 469 | "type": "application/geo+json" 470 | }, 471 | { 472 | "rel": "item", 473 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-52.json", 474 | "type": "application/geo+json" 475 | }, 476 | { 477 | "rel": "item", 478 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-53.json", 479 | "type": "application/geo+json" 480 | }, 481 | { 482 | "rel": "item", 483 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-54.json", 484 | "type": "application/geo+json" 485 | }, 486 | { 487 | "rel": "item", 488 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-55.json", 489 | "type": "application/geo+json" 490 | }, 491 | { 492 | "rel": "item", 493 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-56.json", 494 | "type": "application/geo+json" 495 | }, 496 | { 497 | "rel": "item", 498 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-57.json", 499 | "type": "application/geo+json" 500 | }, 501 | { 502 | "rel": "item", 503 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-58.json", 504 | "type": "application/geo+json" 505 | }, 506 | { 507 | "rel": "item", 508 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-59.json", 509 | "type": "application/geo+json" 510 | }, 511 | { 512 | "rel": "root", 513 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/catalog.json", 514 | "type": "application/json" 515 | }, 516 | { 517 | "rel": "self", 518 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/collection.json", 519 | "type": "application/json", 520 | "title": "EuroSAT STAC Collection with samples from 'train' split.", 521 | "ml-aoi:split": "train" 522 | }, 523 | { 524 | "rel": "collection", 525 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/collection.json", 526 | "type": "application/json", 527 | "title": "EuroSAT STAC Collection with samples from 'train' split.", 528 | "ml-aoi:split": "train" 529 | }, 530 | { 531 | "rel": "related", 532 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/test/collection.json", 533 | "type": "application/json", 534 | "title": "EuroSAT STAC Collection with samples from 'train' split.", 535 | "ml-aoi:split": "test" 536 | }, 537 | { 538 | "rel": "related", 539 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/validate/collection.json", 540 | "type": "application/json", 541 | "title": "EuroSAT STAC Collection with samples from 'train' split.", 542 | "ml-aoi:split": "validate" 543 | }, 544 | { 545 | "rel": "parent", 546 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/catalog.json", 547 | "type": "application/json", 548 | "title": "STAC Catalog" 549 | } 550 | ] 551 | } 552 | -------------------------------------------------------------------------------- /examples/item_EuroSAT-subset-train-sample-42-class-Residential.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [ 4 | "https://stac-extensions.github.io/eo/v1.1.0/schema.json", 5 | "https://stac-extensions.github.io/file/v1.0.0/schema.json", 6 | "https://stac-extensions.github.io/raster/v1.1.0/schema.json", 7 | "https://stac-extensions.github.io/label/v1.0.1/schema.json", 8 | "https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json", 9 | "https://stac-extensions.github.io/version/v1.0.0/schema.json" 10 | ], 11 | "type": "Feature", 12 | "id": "EuroSAT-subset-train-sample-42-class-Residential", 13 | "title": "EuroSAT subset train sample 42 class Residential", 14 | "description": "Annotated sample from the EuroSAT-subset-train collection.", 15 | "bbox": [ 16 | -3.1472537450112785, 17 | 51.531997746547134, 18 | -3.1380012709408427, 19 | 51.53774085855159 20 | ], 21 | "geometry": { 22 | "type": "Polygon", 23 | "coordinates": [ 24 | [ 25 | [ 26 | -3.1380012709408427, 27 | 51.531997746547134 28 | ], 29 | [ 30 | -3.1380012709408427, 31 | 51.53774085855159 32 | ], 33 | [ 34 | -3.1472537450112785, 35 | 51.53774085855159 36 | ], 37 | [ 38 | -3.1472537450112785, 39 | 51.531997746547134 40 | ], 41 | [ 42 | -3.1380012709408427, 43 | 51.531997746547134 44 | ] 45 | ] 46 | ] 47 | }, 48 | "assets": { 49 | "labels": { 50 | "title": "Labels for image Residential_1331 with Residential class", 51 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/data/subset/ds/images/remote_sensing/otherDatasets/sentinel_2/label/Residential/Residential_1331.geojson", 52 | "type": "application/geo+json", 53 | "roles": [ 54 | "data" 55 | ], 56 | "file:size": 749, 57 | "ml-aoi:role": "label" 58 | }, 59 | "raster": { 60 | "title": "Raster Residential_1331 with Residential class", 61 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/data/subset/ds/images/remote_sensing/otherDatasets/sentinel_2/tif/Residential/Residential_1331.tif", 62 | "type": "image/tiff; application=geotiff", 63 | "raster:bands": [ 64 | { 65 | "nodata": 0, 66 | "unit": "m", 67 | "spatial_resolution": 10, 68 | "data_type": "uint16" 69 | }, 70 | { 71 | "nodata": 0, 72 | "unit": "m", 73 | "spatial_resolution": 10, 74 | "data_type": "uint16" 75 | }, 76 | { 77 | "nodata": 0, 78 | "unit": "m", 79 | "spatial_resolution": 10, 80 | "data_type": "uint16" 81 | }, 82 | { 83 | "nodata": 0, 84 | "unit": "m", 85 | "spatial_resolution": 10, 86 | "data_type": "uint16" 87 | }, 88 | { 89 | "nodata": 0, 90 | "unit": "m", 91 | "spatial_resolution": 10, 92 | "data_type": "uint16" 93 | }, 94 | { 95 | "nodata": 0, 96 | "unit": "m", 97 | "spatial_resolution": 10, 98 | "data_type": "uint16" 99 | }, 100 | { 101 | "nodata": 0, 102 | "unit": "m", 103 | "spatial_resolution": 10, 104 | "data_type": "uint16" 105 | }, 106 | { 107 | "nodata": 0, 108 | "unit": "m", 109 | "spatial_resolution": 10, 110 | "data_type": "uint16" 111 | }, 112 | { 113 | "nodata": 0, 114 | "unit": "m", 115 | "spatial_resolution": 10, 116 | "data_type": "uint16" 117 | }, 118 | { 119 | "nodata": 0, 120 | "unit": "m", 121 | "spatial_resolution": 10, 122 | "data_type": "uint16" 123 | }, 124 | { 125 | "nodata": 0, 126 | "unit": "m", 127 | "spatial_resolution": 10, 128 | "data_type": "uint16" 129 | }, 130 | { 131 | "nodata": 0, 132 | "unit": "m", 133 | "spatial_resolution": 10, 134 | "data_type": "uint16" 135 | }, 136 | { 137 | "nodata": 0, 138 | "unit": "m", 139 | "spatial_resolution": 10, 140 | "data_type": "uint16" 141 | } 142 | ], 143 | "roles": [ 144 | "data" 145 | ], 146 | "file:size": 107244, 147 | "ml-aoi:role": "feature", 148 | "ml-aoi:reference-grid": true 149 | }, 150 | "thumbnail": { 151 | "title": "Preview of Residential_1331.", 152 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/data/subset/ds/images/remote_sensing/otherDatasets/sentinel_2/png/Residential/Residential_1331.png", 153 | "type": "image/png", 154 | "roles": [ 155 | "thumbnail", 156 | "visual" 157 | ], 158 | "eo:bands": [ 159 | { 160 | "name": "B04", 161 | "common_name": "red", 162 | "center_wavelength": 0.6645, 163 | "full_width_half_max": 0.038 164 | }, 165 | { 166 | "name": "B03", 167 | "common_name": "green", 168 | "center_wavelength": 0.56, 169 | "full_width_half_max": 0.045 170 | }, 171 | { 172 | "name": "B02", 173 | "common_name": "blue", 174 | "center_wavelength": 0.4966, 175 | "full_width_half_max": 0.098 176 | } 177 | ], 178 | "file:size": 6984 179 | } 180 | }, 181 | "collection": "EuroSAT-subset-train", 182 | "properties": { 183 | "datetime": "2023-12-19T16:11:19.208953+00:00", 184 | "license": "MIT", 185 | "version": "0.5.0", 186 | "label:properties": [ 187 | "class" 188 | ], 189 | "label:tasks": [ 190 | "segmentation", 191 | "classification" 192 | ], 193 | "label:type": "vector", 194 | "label:methods": [ 195 | "manual" 196 | ], 197 | "label:description": "Land-cover area classification on Sentinel-2 image.", 198 | "label:classes": [ 199 | { 200 | "name": "class", 201 | "classes": [ 202 | "Residential", 203 | "7" 204 | ] 205 | } 206 | ], 207 | "label:overviews": [ 208 | { 209 | "property_key": "class", 210 | "counts": [ 211 | { 212 | "name": "Residential", 213 | "count": 1 214 | } 215 | ] 216 | } 217 | ], 218 | "ml-aoi:split": "train", 219 | "constellation": "sentinel-2", 220 | "instruments": [ 221 | "msi" 222 | ], 223 | "eo:bands": [ 224 | { 225 | "name": "B01", 226 | "common_name": "coastal", 227 | "center_wavelength": 0.4439, 228 | "full_width_half_max": 0.027 229 | }, 230 | { 231 | "name": "B02", 232 | "common_name": "blue", 233 | "center_wavelength": 0.4966, 234 | "full_width_half_max": 0.098 235 | }, 236 | { 237 | "name": "B03", 238 | "common_name": "green", 239 | "center_wavelength": 0.56, 240 | "full_width_half_max": 0.045 241 | }, 242 | { 243 | "name": "B04", 244 | "common_name": "red", 245 | "center_wavelength": 0.6645, 246 | "full_width_half_max": 0.038 247 | }, 248 | { 249 | "name": "B05", 250 | "center_wavelength": 0.7039, 251 | "full_width_half_max": 0.019, 252 | "common_name": "rededge" 253 | }, 254 | { 255 | "name": "B06", 256 | "center_wavelength": 0.7402, 257 | "full_width_half_max": 0.018, 258 | "common_name": "rededge" 259 | }, 260 | { 261 | "name": "B07", 262 | "center_wavelength": 0.7825, 263 | "full_width_half_max": 0.028, 264 | "common_name": "rededge" 265 | }, 266 | { 267 | "name": "B08", 268 | "common_name": "nir", 269 | "center_wavelength": 0.8351, 270 | "full_width_half_max": 0.145 271 | }, 272 | { 273 | "name": "B08A", 274 | "center_wavelength": 0.8648, 275 | "full_width_half_max": 0.033, 276 | "common_name": "nir08" 277 | }, 278 | { 279 | "name": "B09", 280 | "center_wavelength": 0.945, 281 | "full_width_half_max": 0.026, 282 | "common_name": "nir09" 283 | }, 284 | { 285 | "name": "B10", 286 | "common_name": "cirrus", 287 | "center_wavelength": 1.3735, 288 | "full_width_half_max": 0.075 289 | }, 290 | { 291 | "name": "B11", 292 | "common_name": "swir16", 293 | "center_wavelength": 1.6137, 294 | "full_width_half_max": 0.143 295 | }, 296 | { 297 | "name": "B12", 298 | "common_name": "swir22", 299 | "center_wavelength": 2.22024, 300 | "full_width_half_max": 0.242 301 | } 302 | ], 303 | "view:off_nadir": 0, 304 | "gsd": 10 305 | }, 306 | "links": [ 307 | { 308 | "title": "Preview of Residential_1331.", 309 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/data/subset/ds/images/remote_sensing/otherDatasets/sentinel_2/png/Residential/Residential_1331.png", 310 | "type": "image/png", 311 | "rel": "thumbnail" 312 | }, 313 | { 314 | "title": "Raster Residential_1331 with Residential class", 315 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/data/subset/ds/images/remote_sensing/otherDatasets/sentinel_2/tif/Residential/Residential_1331.tif", 316 | "type": "image/tiff; application=geotiff", 317 | "rel": "source", 318 | "label:assets": [ 319 | "labels", 320 | "raster" 321 | ], 322 | "ml-aoi:role": "label" 323 | }, 324 | { 325 | "title": "Raster Residential_1331 with Residential class", 326 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/data/subset/ds/images/remote_sensing/otherDatasets/sentinel_2/tif/Residential/Residential_1331.tif", 327 | "type": "image/tiff; application=geotiff", 328 | "rel": "derived_from", 329 | "ml-aoi:role": "feature" 330 | }, 331 | { 332 | "rel": "root", 333 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/catalog.json", 334 | "type": "application/json" 335 | }, 336 | { 337 | "rel": "parent", 338 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/collection.json", 339 | "type": "application/json", 340 | "title": "EuroSAT STAC Collection with samples from 'train' split.", 341 | "ml-aoi:split": "train" 342 | }, 343 | { 344 | "rel": "collection", 345 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/collection.json", 346 | "type": "application/json", 347 | "title": "EuroSAT STAC Collection with samples from 'train' split.", 348 | "ml-aoi:split": "train" 349 | }, 350 | { 351 | "rel": "self", 352 | "href": "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT/stac/subset/train/item-42.json", 353 | "type": "application/geo+json" 354 | } 355 | ] 356 | } 357 | -------------------------------------------------------------------------------- /json-schema/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json#", 4 | "title": "ML AOI Extension", 5 | "description": "ML AOI Extension for STAC definitions.", 6 | "oneOf": [ 7 | { 8 | "$comment": "This is the schema for STAC Collections.", 9 | "allOf": [ 10 | { 11 | "type": "object", 12 | "required": [ 13 | "type" 14 | ], 15 | "properties": { 16 | "type": { 17 | "const": "Collection" 18 | }, 19 | "summaries": { 20 | "type": "object", 21 | "properties": { 22 | "ml-aoi:split": { 23 | "type": "array", 24 | "items": { 25 | "$ref": "#/definitions/fields/properties/ml-aoi:split" 26 | } 27 | } 28 | } 29 | }, 30 | "assets": { 31 | "type": "object", 32 | "additionalProperties": { 33 | "$ref": "#/definitions/fields" 34 | } 35 | }, 36 | "item_assets": { 37 | "type": "object", 38 | "additionalProperties": { 39 | "$ref": "#/definitions/fields" 40 | } 41 | } 42 | } 43 | }, 44 | { 45 | "$ref": "#/definitions/stac_extensions" 46 | } 47 | ] 48 | }, 49 | { 50 | "$comment": "This is the schema for STAC Items.", 51 | "allOf": [ 52 | { 53 | "type": "object", 54 | "required": [ 55 | "type", 56 | "properties", 57 | "assets" 58 | ], 59 | "properties": { 60 | "type": { 61 | "const": "Feature" 62 | }, 63 | "properties": { 64 | "allOf": [ 65 | { 66 | "$ref": "#/definitions/fields" 67 | } 68 | ] 69 | }, 70 | "assets": { 71 | "type": "object", 72 | "additionalProperties": { 73 | "$ref": "#/definitions/fields" 74 | } 75 | } 76 | } 77 | }, 78 | { 79 | "$ref": "#/definitions/stac_extensions" 80 | } 81 | ] 82 | } 83 | ], 84 | "definitions": { 85 | "stac_extensions": { 86 | "type": "object", 87 | "required": [ 88 | "stac_extensions" 89 | ], 90 | "properties": { 91 | "stac_extensions": { 92 | "type": "array", 93 | "contains": { 94 | "const": "https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json" 95 | } 96 | } 97 | } 98 | }, 99 | "fields": { 100 | "type": "object", 101 | "properties": { 102 | "ml-aoi:split": { 103 | "type": "string", 104 | "enum": ["train", "test", "validate"] 105 | }, 106 | "ml-aoi:role": { 107 | "type": "string", 108 | "enum": ["label", "feature"] 109 | }, 110 | "ml-aoi:reference-grid": { 111 | "type": "boolean" 112 | }, 113 | "ml-aoi:resampling-method": { 114 | "$comment": "Supported GDAL resampling method (https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r)", 115 | "type": "string", 116 | "enum": [ 117 | "near", 118 | "bilinear", 119 | "cubic", 120 | "cubcspline", 121 | "lanczos", 122 | "average", 123 | "rms", 124 | "mode", 125 | "max", 126 | "min", 127 | "med", 128 | "q1", 129 | "q3", 130 | "sum" 131 | ] 132 | } 133 | }, 134 | "patternProperties": { 135 | "^(?!ml-aoi:)": {} 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stac-extensions", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "test": "npm run check-markdown && npm run check-examples", 6 | "check-markdown": "remark . -f -r .github/remark.yaml", 7 | "check-examples": "stac-node-validator . --lint --verbose --schemaMap https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json=./json-schema/schema.json", 8 | "format-examples": "stac-node-validator . --format --schemaMap https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json=./json-schema/schema.json" 9 | }, 10 | "dependencies": { 11 | "remark-lint-no-html": "^2.0.0", 12 | "remark-preset-lint-consistent": "^3.0.0", 13 | "remark-validate-links": "^10.0.0", 14 | "stac-node-validator": "^1.0.0" 15 | }, 16 | "devDependencies": { 17 | "remark-cli": "^12.0.0", 18 | "remark-gfm": "^4.0.0", 19 | "remark-lint": "^9.1.2", 20 | "remark-lint-checkbox-content-indent": "^4.1.2", 21 | "remark-lint-maximum-line-length": "^3.1.3", 22 | "remark-message-control": "^8.0.0", 23 | "remark-preset-lint-markdown-style-guide": "^5.1.3", 24 | "remark-preset-lint-recommended": "^6.1.3", 25 | "stylelint": "^15.11.0", 26 | "stylelint-config-standard": "^34.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pystac_ml_aoi/__init__.py: -------------------------------------------------------------------------------- 1 | # this is automatically updated by 'make bump' 2 | __version__ = "0.2.0" 3 | -------------------------------------------------------------------------------- /pystac_ml_aoi/extensions/ml_aoi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities to extend :mod:`pystac` objects with STAC ML-AOI extension. 5 | """ 6 | import abc 7 | import json 8 | import os 9 | from typing import Any, Generic, Iterable, List, Literal, Optional, Protocol, TypeVar, Union, cast, get_args 10 | 11 | import pystac 12 | from pydantic import BaseModel, ConfigDict, Field, model_validator 13 | from pydantic.fields import FieldInfo 14 | from pystac.extensions import item_assets 15 | from pystac.extensions.base import ( # generic pystac.STACObject 16 | ExtensionManagementMixin, 17 | PropertiesExtension, 18 | S, 19 | SummariesExtension 20 | ) 21 | from pystac.extensions.hooks import ExtensionHooks 22 | from pystac.utils import StringEnum 23 | 24 | T = TypeVar("T", pystac.Collection, pystac.Item, pystac.Asset, item_assets.AssetDefinition) 25 | SchemaName = Literal["ml-aoi"] 26 | 27 | AnySummary = Union[list[Any], pystac.RangeSummary[Any], dict[str, Any], None] 28 | 29 | _PROJECT_ROOT = os.path.abspath(os.path.join(__file__, "../../..")) 30 | ML_AOI_SCHEMA_PATH = os.path.join(_PROJECT_ROOT, "json-schema/schema.json") 31 | 32 | with open(ML_AOI_SCHEMA_PATH, mode="r", encoding="utf-8") as schema_file: 33 | ML_AOI_SCHEMA = json.load(schema_file) 34 | 35 | ML_AOI_SCHEMA_ID: SchemaName = get_args(SchemaName)[0] 36 | ML_AOI_SCHEMA_URI: str = ML_AOI_SCHEMA["$id"].split("#")[0] 37 | ML_AOI_PREFIX = f"{ML_AOI_SCHEMA_ID}:" 38 | ML_AOI_PROPERTY = f"{ML_AOI_SCHEMA_ID}_".replace("-", "_") 39 | 40 | 41 | class ExtendedEnum(StringEnum): 42 | @classmethod 43 | def values(cls) -> List[str]: 44 | return list(cls.__members__.values()) 45 | 46 | 47 | class ML_AOI_Split(ExtendedEnum): 48 | TRAIN: Literal["train"] = "train" 49 | VALIDATE: Literal["validate"] = "validate" 50 | TEST: Literal["test"] = "test" 51 | 52 | 53 | ML_AOI_SplitType = Union[ 54 | ML_AOI_Split, 55 | Literal[ 56 | ML_AOI_Split.TRAIN, 57 | ML_AOI_Split.VALIDATE, 58 | ML_AOI_Split.TEST, 59 | ] 60 | ] 61 | 62 | 63 | class ML_AOI_Role(ExtendedEnum): 64 | LABEL: Literal["label"] = "label" 65 | FEATURE: Literal["feature"] = "feature" 66 | 67 | 68 | ML_AOI_RoleType = Union[ 69 | ML_AOI_Role, 70 | Literal[ 71 | ML_AOI_Role.LABEL, 72 | ML_AOI_Role.FEATURE, 73 | ] 74 | ] 75 | 76 | 77 | class ML_AOI_Resampling(ExtendedEnum): 78 | NEAR: Literal["near"] = "near" 79 | BILINEAR: Literal["bilinear"] = "bilinear" 80 | CUBIC: Literal["cubic"] = "cubic" 81 | CUBCSPLINE: Literal["cubcspline"] = "cubcspline" 82 | LANCZOS: Literal["lanczos"] = "lanczos" 83 | AVERAGE: Literal["average"] = "average" 84 | RMS: Literal["rms"] = "rms" 85 | MODE: Literal["mode"] = "mode" 86 | MAX: Literal["max"] = "max" 87 | MIN: Literal["min"] = "min" 88 | MED: Literal["med"] = "med" 89 | Q1: Literal["q1"] = "q1" 90 | Q3: Literal["q3"] = "q3" 91 | SUM: Literal["sum"] = "sum" 92 | 93 | 94 | ML_AOI_ResamplingType = Union[ 95 | Literal[ 96 | ML_AOI_Resampling.NEAR, 97 | ML_AOI_Resampling.BILINEAR, 98 | ML_AOI_Resampling.CUBIC, 99 | ML_AOI_Resampling.CUBCSPLINE, 100 | ML_AOI_Resampling.LANCZOS, 101 | ML_AOI_Resampling.AVERAGE, 102 | ML_AOI_Resampling.RMS, 103 | ML_AOI_Resampling.MODE, 104 | ML_AOI_Resampling.MAX, 105 | ML_AOI_Resampling.MIN, 106 | ML_AOI_Resampling.MED, 107 | ML_AOI_Resampling.Q1, 108 | ML_AOI_Resampling.Q3, 109 | ML_AOI_Resampling.SUM, 110 | ] 111 | ] 112 | 113 | # pystac references 114 | SCHEMA_URI = ML_AOI_SCHEMA_URI 115 | SCHEMA_URIS = [SCHEMA_URI] 116 | PREFIX = ML_AOI_PREFIX 117 | 118 | 119 | def add_ml_aoi_prefix(name: str) -> str: 120 | return ML_AOI_PREFIX + name if "datetime" not in name else name 121 | 122 | 123 | class ML_AOI_BaseFields(BaseModel, validate_assignment=True): 124 | """ 125 | ML-AOI base definition to validate fields and properties. 126 | """ 127 | 128 | model_config = ConfigDict(alias_generator=add_ml_aoi_prefix, populate_by_name=True, extra="ignore") 129 | 130 | @model_validator(mode="after") 131 | def validate_one_of(self) -> "ML_AOI_BaseFields": 132 | """ 133 | All fields are optional, but at least one is required. 134 | 135 | Additional ``ml-aoi:`` prefixed properties are allowed as well, but no additional validation is performed. 136 | """ 137 | # note: 138 | # purposely omit 'by_alias=True' to have any fields defined, with/without extension prefix 139 | # if the extension happened to use any non-prefixed field, they would be validated as well 140 | fields = self.model_dump() 141 | # fields = { 142 | # f: v for f, v in fields.items() 143 | # if f.startswith(ML_AOI_PREFIX) and f.replace(ML_AOI_PREFIX, "") 144 | # } 145 | if len(fields) < 1 or all(value is None for value in fields.values()): 146 | raise ValueError("ML-AOI extension must provide at least one valid field.") 147 | return self 148 | 149 | 150 | class ML_AOI_ItemProperties(ML_AOI_BaseFields, validate_assignment=True): 151 | """ 152 | ML-AOI properties for STAC Items. 153 | """ 154 | 155 | split: Optional[ML_AOI_Split] # split is required since it is the only available field 156 | 157 | 158 | class ML_AOI_CollectionFields(ML_AOI_BaseFields, validate_assignment=True): 159 | """ 160 | ML-AOI properties for STAC Collections. 161 | """ 162 | 163 | split: Optional[List[ML_AOI_Split]] # split is required since it is the only available field 164 | 165 | 166 | class ML_AOI_AssetFields(ML_AOI_BaseFields, validate_assignment=True): 167 | role: Optional[ML_AOI_Role] = None 168 | reference_grid: Optional[bool] = Field(alias="reference-grid", default=None) 169 | resampling_method: Optional[ML_AOI_Resampling] = Field( 170 | alias="resampling-method", 171 | description="Supported GDAL resampling method (https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r)", 172 | default=None, 173 | ) 174 | 175 | 176 | class ML_AOI_LinkFields(ML_AOI_BaseFields, validate_assignment=True): 177 | role: Optional[ML_AOI_Role] # role is required since it is the only available field 178 | 179 | 180 | # class ML_AOI_MetaClass(type, abc.ABC): 181 | # @property 182 | # @abc.abstractmethod 183 | # def model(self) -> ML_AOI_BaseFields: 184 | # raise NotImplementedError 185 | 186 | class ML_AOI_MetaClass(Protocol): 187 | model: ML_AOI_BaseFields 188 | 189 | 190 | class ML_AOI_Extension( 191 | ML_AOI_MetaClass, 192 | Generic[T], 193 | ExtensionManagementMixin[Union[pystac.Asset, pystac.Item, pystac.Collection]], 194 | abc.ABC, 195 | ): 196 | @abc.abstractmethod 197 | def get_ml_aoi_property(self, prop_name: str, *, _ml_aoi_required: bool) -> Any: 198 | raise NotImplementedError 199 | 200 | @abc.abstractmethod 201 | def set_ml_aoi_property( 202 | self, 203 | prop_name: str, 204 | value: Any, 205 | pop_if_none: bool = True, 206 | *, 207 | _ml_aoi_required: bool, 208 | ) -> None: 209 | raise NotImplementedError 210 | 211 | def __getitem__(self, prop_name): 212 | return self.get_ml_aoi_property(prop_name, _ml_aoi_required=False) 213 | 214 | def __setattr__(self, prop_name, value): 215 | self.set_ml_aoi_property(prop_name, value, _ml_aoi_required=False, pop_if_none=True) 216 | 217 | @classmethod 218 | def _is_ml_aoi_property(cls, prop_name: str): 219 | return ( 220 | prop_name.startswith(ML_AOI_PREFIX) 221 | or prop_name.startswith(ML_AOI_PROPERTY) 222 | or prop_name in cls.model.model_fields 223 | ) 224 | 225 | @classmethod 226 | def _retrieve_ml_aoi_property(cls, prop_name: str, *, _ml_aoi_required: bool) -> Optional[FieldInfo]: 227 | if not _ml_aoi_required and not cls._is_ml_aoi_property(prop_name): 228 | return 229 | try: 230 | return cls.model.model_fields[prop_name] 231 | except KeyError: 232 | raise AttributeError(f"Name '{prop_name}' is not a valid ML-AOI field.") 233 | 234 | @classmethod 235 | def _validate_ml_aoi_property(cls, prop_name: str, value: Any) -> None: 236 | model = cls.model.model_construct() 237 | validator = cls.model.__pydantic_validator__ 238 | validator.validate_assignment(model, prop_name, value) 239 | 240 | @property 241 | def name(self) -> SchemaName: 242 | return ML_AOI_SCHEMA_ID 243 | 244 | def apply( 245 | self, 246 | fields: Union[ 247 | ML_AOI_CollectionFields, 248 | ML_AOI_ItemProperties, 249 | ML_AOI_AssetFields, 250 | ML_AOI_LinkFields, 251 | dict[str, Any] 252 | ] = None, 253 | **extra_fields: Any, 254 | ) -> None: 255 | """ 256 | Applies ML-AOI Extension properties to the extended :class:`~pystac.Item` or :class:`~pystac.Asset`. 257 | """ 258 | if not fields: 259 | fields = {} 260 | fields.update(extra_fields) 261 | obj = ( 262 | getattr(self, "collection", None) or 263 | getattr(self, "item", None) or 264 | getattr(self, "asset", None) or 265 | getattr(self, "link") 266 | ) 267 | if isinstance(fields, dict): 268 | if isinstance(obj, pystac.Collection): 269 | fields = ML_AOI_CollectionFields(**fields) 270 | elif isinstance(obj, pystac.Item): 271 | fields = ML_AOI_ItemProperties(**fields) 272 | elif isinstance(obj, pystac.Asset): 273 | fields = ML_AOI_AssetFields(**fields) 274 | elif isinstance(obj, pystac.Link): 275 | fields = ML_AOI_LinkFields(**fields) 276 | else: 277 | raise pystac.ExtensionTypeError(self._ext_error_message(obj)) 278 | elif not ( 279 | (isinstance(obj, pystac.Collection) and not isinstance(fields, ML_AOI_CollectionFields)) or 280 | (isinstance(obj, pystac.Item) and not isinstance(fields, ML_AOI_ItemProperties)) or 281 | (isinstance(obj, pystac.Asset) and not isinstance(fields, ML_AOI_AssetFields)) or 282 | (isinstance(obj, pystac.Link) and not isinstance(fields, ML_AOI_LinkFields)) 283 | ): 284 | raise pystac.ExtensionTypeError( 285 | f"Cannot use {fields.__class__.__name__} with STAC Object {obj.STAC_OBJECT_TYPE}" 286 | ) 287 | data_json = json.loads(fields.model_dump_json(by_alias=False)) 288 | prop_setter = getattr(self, "_set_property", None) or getattr(self, "_set_summary", None) 289 | if not prop_setter: 290 | raise pystac.ExtensionTypeError( 291 | f"Invalid {self._ext_error_message(self)} implementation " 292 | f"does not provide any property or summary setter method." 293 | ) 294 | for field, val in data_json.items(): 295 | prop_setter(field, val) 296 | 297 | @classmethod 298 | def get_schema_uri(cls) -> str: 299 | return SCHEMA_URI 300 | 301 | @classmethod 302 | def has_extension(cls, obj: S): 303 | ext_uri = cls.get_schema_uri() 304 | return obj.stac_extensions is not None and any(uri == ext_uri for uri in obj.stac_extensions) 305 | 306 | @classmethod 307 | def ext(cls, obj: T, add_if_missing: bool = False) -> "ML_AOI_Extension[T]": 308 | """ 309 | Extends the given STAC Object with properties from the :stac-ext:`ML-AOI Extension `. 310 | 311 | This extension can be applied to instances of :class:`~pystac.Item` or 312 | :class:`~pystac.Asset`. 313 | 314 | Raises: 315 | pystac.ExtensionTypeError : If an invalid object type is passed. 316 | """ 317 | if isinstance(obj, pystac.Collection): 318 | cls.ensure_has_extension(obj, add_if_missing) 319 | return cast(ML_AOI_Extension[T], ML_AOI_CollectionExtension(obj)) 320 | elif isinstance(obj, pystac.Item): 321 | cls.ensure_has_extension(obj, add_if_missing) 322 | return cast(ML_AOI_Extension[T], ML_AOI_ItemExtension(obj)) 323 | elif isinstance(obj, pystac.Asset): 324 | cls.ensure_owner_has_extension(obj, add_if_missing) 325 | return cast(ML_AOI_Extension[T], ML_AOI_AssetExtension(obj)) 326 | # elif isinstance(obj, item_assets.AssetDefinition): 327 | # cls.ensure_owner_has_extension(obj, add_if_missing) 328 | # return cast(ML_AOI_Extension[T], ItemAssetsML_AOI_Extension(obj)) 329 | else: 330 | raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) 331 | 332 | @classmethod 333 | def summaries(cls, obj: pystac.Collection, add_if_missing: bool = False) -> "ML_AOI_SummariesExtension": 334 | """ 335 | Returns the extended summaries object for the given collection. 336 | """ 337 | cls.ensure_has_extension(obj, add_if_missing) 338 | return ML_AOI_SummariesExtension(obj) 339 | 340 | 341 | class ML_AOI_PropertiesExtension( 342 | PropertiesExtension, 343 | ML_AOI_Extension[T], 344 | abc.ABC, 345 | ): 346 | def get_ml_aoi_property(self, prop_name: str, *, _ml_aoi_required: bool = True) -> list[Any]: 347 | self._retrieve_ml_aoi_property(prop_name, _ml_aoi_required=_ml_aoi_required) 348 | return self.properties.get(prop_name) 349 | 350 | def set_ml_aoi_property( 351 | self, 352 | prop_name: str, 353 | value: Any, 354 | *, 355 | _ml_aoi_required: bool = True, 356 | pop_if_none: bool = True, 357 | ) -> None: 358 | field = self._retrieve_ml_aoi_property(prop_name, _ml_aoi_required=_ml_aoi_required) 359 | if field: 360 | # validation must be performed against the non-aliased field 361 | # then, apply the alias for the actual assignment of the property 362 | self._validate_ml_aoi_property(prop_name, value) 363 | prop_name = field.alias or prop_name 364 | if prop_name in dir(self) or prop_name in self.__annotations__: 365 | object.__setattr__(self, prop_name, value) 366 | else: 367 | super()._set_property(prop_name, value, pop_if_none=pop_if_none) 368 | 369 | def _set_property( 370 | self, prop_name: str, v: Any, pop_if_none: bool = True 371 | ) -> None: 372 | if self._is_ml_aoi_property(prop_name): 373 | self.set_ml_aoi_property(prop_name, v, _ml_aoi_required=True) 374 | else: 375 | super()._set_property(prop_name, v, pop_if_none=pop_if_none) 376 | 377 | 378 | class ML_AOI_ItemExtension( 379 | ML_AOI_PropertiesExtension, 380 | ML_AOI_Extension[pystac.Item], 381 | ): 382 | """ 383 | STAC Item extension for ML-AOI. 384 | 385 | A concrete implementation of :class:`ML_AOI_Extension` on an :class:`~pystac.Item` that extends the properties of 386 | the Item to include properties defined in the :stac-ext:`ML-AOI Extension `. 387 | 388 | This class should generally not be instantiated directly. Instead, call 389 | :meth:`ML_AOI_Extension.ext` on an :class:`~pystac.Item` to extend it. 390 | """ 391 | model = ML_AOI_ItemProperties 392 | item: pystac.Item 393 | properties: dict[str, Any] 394 | 395 | def __init__(self, item: pystac.Item): 396 | self.properties = item.properties 397 | self.item = item 398 | 399 | def get_assets( 400 | self, 401 | role: Optional[Union[ML_AOI_Role, List[ML_AOI_Role]]] = None, 402 | reference_grid: Optional[bool] = None, 403 | resampling_method: Optional[str] = None, 404 | media_type: Optional[Union[pystac.MediaType, str]] = None, 405 | asset_role: Optional[str] = None, 406 | ) -> dict[str, pystac.Asset]: 407 | """ 408 | Get the item's assets where ``ml-aoi`` fields are matched. 409 | 410 | Args: 411 | role: The ML-AOI role, or a list of roles to filter Assets. 412 | Note, this should not be confused with Asset 'roles'. 413 | reference_grid: Filter Assets that contain the specified ML-AOI reference grid value. 414 | resampling_method: Filter Assets that contain the specified ML-AOI resampling method. 415 | media_type: Filter Assets with the given media-type. 416 | asset_role: Filter Assets which contains the specified role (not to be confused with the ML-AOI role). 417 | Returns: 418 | Dict[str, Asset]: A dictionary of assets that matched filters. 419 | """ 420 | # since this method could be used for assets that refer to other extensions as well, 421 | # filters must not limit themselves to ML-AOI fields 422 | # if values are 'None', we must consider them as 'ignore' instead of 'any of' allowed values 423 | return { 424 | key: asset 425 | for key, asset in self.item.get_assets(media_type=media_type, role=asset_role).items() 426 | if ( 427 | not role or 428 | any( 429 | asset_role in (asset.extra_fields.get(add_ml_aoi_prefix("role")) or []) 430 | for asset_role in ([role] if isinstance(role, ML_AOI_Role) else role) 431 | ) 432 | ) 433 | and ( 434 | reference_grid is None or 435 | asset.extra_fields.get(add_ml_aoi_prefix("reference-grid")) == reference_grid 436 | ) 437 | and ( 438 | resampling_method is None or 439 | asset.extra_fields.get(add_ml_aoi_prefix("resampling-method")) == resampling_method 440 | ) 441 | } 442 | 443 | def __repr__(self) -> str: 444 | return f"" 445 | 446 | 447 | # class ItemAssetsML_AOI_Extension(ML_AOI_Extension[item_assets.AssetDefinition]): 448 | # """A concrete implementation of :class:`ML_AOI_Extension` on an :class:`~pystac.Asset` 449 | # that extends the Asset fields to include properties defined in the 450 | # :stac-ext:`ML-AOI Extension `. 451 | # 452 | # This class should generally not be instantiated directly. Instead, call 453 | # :meth:`ML_AOI_Extension.ext` on an :class:`~pystac.Asset` to extend it. 454 | # """ 455 | # properties: dict[str, Any] 456 | # asset_defn: item_assets.AssetDefinition 457 | # 458 | # def __init__(self, item_asset: item_assets.AssetDefinition): 459 | # self.asset_defn = item_asset 460 | # self.properties = item_asset.properties 461 | # 462 | # def __repr__(self) -> str: 463 | # return f"" 464 | 465 | 466 | class ML_AOI_AssetExtension( 467 | ML_AOI_PropertiesExtension, 468 | ML_AOI_Extension[pystac.Asset], 469 | ): 470 | """ 471 | Asset extension for ML-AOI. 472 | 473 | A concrete implementation of :class:`ML_AOI_Extension` on an :class:`~pystac.Asset` that extends the Asset fields to 474 | include properties defined in the :stac-ext:`ML-AOI Extension `. 475 | 476 | This class should generally not be instantiated directly. Instead, call 477 | :meth:`ML_AOI_Extension.ext` on an :class:`~pystac.Asset` to extend it. 478 | """ 479 | model = ML_AOI_AssetFields 480 | 481 | asset_href: str 482 | """ 483 | The ``href`` value of the :class:`~pystac.Asset` being extended. 484 | """ 485 | 486 | properties: dict[str, Any] 487 | """ 488 | The :class:`~pystac.Asset` fields, including extension properties. 489 | """ 490 | 491 | additional_read_properties: Optional[Iterable[dict[str, Any]]] = None 492 | """ 493 | If present, this will be a list containing 1 dictionary representing the properties of the owning 494 | :class:`~pystac.Item`. 495 | """ 496 | 497 | def __init__(self, asset: pystac.Asset): 498 | self.asset_href = asset.href 499 | self.properties = asset.extra_fields 500 | if asset.owner and isinstance(asset.owner, pystac.Item): 501 | self.additional_read_properties = [asset.owner.properties] 502 | 503 | def __repr__(self) -> str: 504 | return f"" 505 | # 506 | # @property 507 | # def role(self) -> Optional[ML_AOI_Role]: 508 | # return self._get_property(add_ml_aoi_prefix("role"), ML_AOI_Role) 509 | # 510 | # @role.setter 511 | # def role(self, role: ML_AOI_Role) -> None: 512 | # self._set_property(add_ml_aoi_prefix("role"), role) 513 | # 514 | # @property 515 | # def reference_grid(self) -> Optional[bool]: 516 | # return self._get_property(add_ml_aoi_prefix("reference-grid"), bool) 517 | # 518 | # @reference_grid.setter 519 | # def reference_grid(self, reference_grid: bool) -> None: 520 | # self._set_property(add_ml_aoi_prefix("reference-grid"), reference_grid) 521 | 522 | 523 | class ML_AOI_SummariesExtension( 524 | SummariesExtension, 525 | ML_AOI_Extension[pystac.Collection], 526 | ): 527 | """ 528 | Summaries extension for ML-AOI. 529 | 530 | A concrete implementation of :class:`~SummariesExtension` that extends the ``summaries`` field of a 531 | :class:`~pystac.Collection` to include properties defined in the :stac-ext:`ML-AOI `. 532 | """ 533 | model = ML_AOI_CollectionFields 534 | 535 | collection: pystac.Collection 536 | summaries: pystac.Summaries 537 | 538 | def __init__(self, collection: pystac.Collection): 539 | self.collection = collection 540 | super().__init__(collection) 541 | 542 | def get_ml_aoi_property( 543 | self, 544 | prop_name: str, 545 | *, 546 | _ml_aoi_required: bool = True, 547 | ) -> AnySummary: 548 | field = self._retrieve_ml_aoi_property(prop_name, _ml_aoi_required=_ml_aoi_required) 549 | if field or _ml_aoi_required: 550 | prop_name = field.alias or prop_name 551 | return self.summaries.get_list(prop_name) 552 | return object.__getattribute__(self, prop_name) 553 | 554 | def set_ml_aoi_property( 555 | self, 556 | prop_name: str, 557 | value: AnySummary, 558 | pop_if_none: bool = False, 559 | *, 560 | _ml_aoi_required: bool = True, 561 | ) -> None: 562 | # if _ml_aoi_required and not hasattr(value, "__iter__") or isinstance(value, str): 563 | # raise pystac.ExtensionTypeError( 564 | # "Summaries value must be an iterable container such as a list, " 565 | # f"received '{value.__class__.__name__}' is invalid." 566 | # ) 567 | 568 | field = self._retrieve_ml_aoi_property(prop_name, _ml_aoi_required=_ml_aoi_required) 569 | if field or _ml_aoi_required: 570 | # prop_name = field.alias or prop_name 571 | if not isinstance(value, (list, pystac.RangeSummary, dict)): 572 | value = [value] 573 | self._validate_ml_aoi_property(prop_name, value) 574 | prop_name = field.alias or prop_name 575 | super()._set_summary(prop_name, value) 576 | else: 577 | object.__setattr__(self, prop_name, value) 578 | 579 | def _set_summary( 580 | self, 581 | prop_name: str, 582 | summaries: Union[list[Any], pystac.RangeSummary[Any], dict[str, Any], None], 583 | ) -> None: 584 | if self._is_ml_aoi_property(prop_name): 585 | self.set_ml_aoi_property(prop_name, summaries, _ml_aoi_required=True) 586 | else: 587 | super()._set_summary(prop_name, summaries) 588 | 589 | 590 | class ML_AOI_CollectionExtension( 591 | ML_AOI_SummariesExtension, 592 | ML_AOI_Extension[pystac.Collection] 593 | ): 594 | model = ML_AOI_CollectionFields 595 | 596 | def __init__(self, collection: pystac.Collection): 597 | ML_AOI_SummariesExtension.__init__(self, collection) 598 | self.collection = collection 599 | self.properties = collection.extra_fields 600 | self.collection.set_self_href = ML_AOI_CollectionExtension.set_self_href # override hook 601 | 602 | def __repr__(self) -> str: 603 | return f"" 604 | 605 | def set_self_href(self, href: Optional[str]) -> None: 606 | """ 607 | Sets the absolute HREF that is represented by the ``rel == 'self'`` :class:`~pystac.Link`. 608 | 609 | Adds the relevant ML-AOI role applicable for the Collection. 610 | """ 611 | pystac.Collection.set_self_href(self.collection, href) 612 | ml_aoi_split = self.get_ml_aoi_property("split") 613 | if not ml_aoi_split: 614 | return 615 | for link in self.collection.links: 616 | if link.rel == pystac.RelType.SELF: 617 | field_name = add_ml_aoi_prefix("split") 618 | link.extra_fields[field_name] = ml_aoi_split[0] 619 | 620 | 621 | class ML_AOI_ExtensionHooks(ExtensionHooks): 622 | schema_uri: str = SCHEMA_URI 623 | prev_extension_ids = { 624 | ML_AOI_SCHEMA_ID, 625 | *[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI], 626 | } 627 | stac_object_types = { 628 | pystac.STACObjectType.COLLECTION, 629 | pystac.STACObjectType.ITEM, 630 | } 631 | 632 | 633 | ML_AOI_EXTENSION_HOOKS: ExtensionHooks = ML_AOI_ExtensionHooks() 634 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | bandit 2 | bump2version 3 | doc8 4 | docformatter 5 | flake8 6 | flynt 7 | isort 8 | pydocstyle 9 | pylint<3 10 | pylint-per-file-ignores 11 | pylint_quotes 12 | pystac>=1.10.0 # requried for custom STAC validator (see https://github.com/stac-utils/pystac/pull/1320) 13 | pytest 14 | safety 15 | -------------------------------------------------------------------------------- /requirements-sys.txt: -------------------------------------------------------------------------------- 1 | pip>=22.0.4 2 | setuptools>=65.5.1 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jsonschema 2 | pystac 3 | shapely 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.0 3 | commit = True 4 | tag = True 5 | tag_name = v{new_version} 6 | 7 | [bumpversion:file:CHANGELOG.md] 8 | search = 9 | [Unreleased](https://github.com/stac-extensions/ml-aoi/compare/v{current_version}...HEAD) (latest) 10 | --------------------------------------------------------------------------------------- 11 | replace = 12 | [Unreleased](https://github.com/stac-extensions/ml-aoi/compare/v{new_version}...HEAD) (latest) 13 | --------------------------------------------------------------------------------------- 14 | 15 | \### Added 16 | - n/a 17 | 18 | \### Changed 19 | - n/a 20 | 21 | \### Deprecated 22 | - n/a 23 | 24 | \### Removed 25 | - n/a 26 | 27 | \### Fixed 28 | - n/a 29 | 30 | .. _changes_{new_version}: 31 | 32 | [v{new_version}](https://github.com/stac-extensions/ml-aoi/tree/v{new_version}) ({now:%%Y-%%m-%%d}) 33 | --------------------------------------------------------------------------------------- 34 | 35 | [bumpversion:file:README.md] 36 | search = {current_version} 37 | replace = {new_version} 38 | 39 | [bumpversion:file:setup.py] 40 | search = VERSION = "{current_version}" 41 | replace = VERSION = "{new_version}" 42 | 43 | [bumpversion:file:pystac_ml_aoi/__init__.py] 44 | search = __version__ = "{current_version}" 45 | replace = __version__ = "{new_version}" 46 | 47 | [bumpversion:file:Makefile] 48 | search = APP_VERSION ?= {current_version} 49 | replace = APP_VERSION ?= {new_version} 50 | 51 | [bumpversion:file:package.json] 52 | search = ml-aoi/v{current_version} 53 | replace = ml-aoi/v{new_version} 54 | 55 | [bumpversion:glob:examples/*.json] 56 | search = ml-aoi/v{current_version} 57 | replace = ml-aoi/v{new_version} 58 | 59 | [bumpversion:glob:examples/*.geojson] 60 | search = ml-aoi/v{current_version} 61 | replace = ml-aoi/v{new_version} 62 | 63 | [bumpversion:glob:json-schema/*.json] 64 | search = ml-aoi/v{current_version} 65 | replace = ml-aoi/v{new_version} 66 | 67 | [tool:pytest] 68 | addopts = 69 | --strict-markers 70 | --tb=native 71 | --ignore=tests/smoke 72 | pystac_ml_aoi/ 73 | log_cli = false 74 | log_level = DEBUG 75 | python_files = test_*.py 76 | markers = 77 | functional: mark test as functionality validation 78 | filterwarnings = 79 | ignore:.*iana\.org.*:urllib3.exceptions.InsecureRequestWarning 80 | 81 | [isort] 82 | line_length = 120 83 | multi_line_output = 3 84 | lines_between_types = 0 85 | lines_between_sections = 1 86 | combine_as_imports = true 87 | order_by_type = true 88 | classes = CWL,JSON,KVP,IO 89 | treat_all_comments_as_code = true 90 | default_section = THIRDPARTY 91 | sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 92 | extra_standard_library = posixpath,typing,typing_extensions 93 | known_third_party = cornice_swagger,cwltool,cwt,docker,mock 94 | known_first_party = pystac_ml_aoi,tests 95 | skip = *.egg*,build,env,src,venv,reports,node_modules 96 | 97 | [bandit] 98 | skips = B101,B320,B410 99 | exclude = *.egg-info,./build,./dist,./env,./tests,test_* 100 | targets = . 101 | 102 | [flake8] 103 | ignore = E126,E226,E402,E501,F401,W503,W504,B007,B009,B010,B023 104 | max-line-length = 120 105 | exclude = 106 | src, 107 | .git, 108 | __pycache__, 109 | docs, 110 | build, 111 | dist, 112 | eggs, 113 | env, 114 | parts, 115 | examples, 116 | node_modules, 117 | 118 | [doc8] 119 | max-line-length = 120 120 | ignore-path = docs/build,docs/source/autoapi 121 | 122 | [docformatter] 123 | recursive = true 124 | wrap-descriptions = 0 125 | wrap-summaries = 120 126 | make-summary-multi-line = True 127 | pre-summary-newline = True 128 | 129 | [pydocstyle] 130 | add_ignore = D100,D101,D102,D103,D104,D105,D107,D200,D202,D204,D212,D401 131 | add_select = D201,D213 132 | 133 | [pylint] 134 | 135 | [coverage:run] 136 | branch = true 137 | source = ./ 138 | include = pystac_ml_aoi/* 139 | omit = 140 | setup.py 141 | docs/* 142 | tests/* 143 | *_mako 144 | 145 | [coverage:report] 146 | exclude_lines = 147 | pragma: no cover 148 | raise OSError 149 | raise AssertionError 150 | raise NotImplementedError 151 | if TYPE_CHECKING: 152 | if __name__ == "__main__": 153 | LOGGER.debug 154 | LOGGER.info 155 | LOGGER.warning 156 | LOGGER.error 157 | LOGGER.exception 158 | LOGGER.log 159 | self.logger.debug 160 | self.logger.info 161 | self.logger.warning 162 | self.logger.error 163 | self.logger.exception 164 | self.logger.log 165 | @overload 166 | if not result.success: 167 | raise PackageAuthenticationError 168 | raise PackageExecutionError 169 | raise PackageNotFound 170 | raise PackageParsingError 171 | raise PackageRegistrationError 172 | raise PackageTypeError 173 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from typing import Optional, Set, Tuple 5 | 6 | from setuptools import find_packages, setup 7 | 8 | CUR_DIR = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | # ensure that 'weaver' directory can be found for metadata import 11 | sys.path.insert(0, CUR_DIR) 12 | sys.path.insert(0, os.path.join(CUR_DIR, os.path.split(CUR_DIR)[-1])) 13 | 14 | CUR_PKG = os.path.basename(CUR_DIR) 15 | LOGGER = logging.getLogger(f"{CUR_PKG}.setup") 16 | if logging.StreamHandler not in LOGGER.handlers: 17 | LOGGER.addHandler(logging.StreamHandler(sys.stdout)) # type: ignore # noqa 18 | LOGGER.setLevel(logging.INFO) 19 | LOGGER.info("starting setup") 20 | 21 | 22 | def read_doc_file(*file_names: str) -> Optional[Tuple[str, str]]: 23 | for file_name in file_names: 24 | for ext, ctype in [("md", "text/markdown"), ("rst", "text/x-rst")]: 25 | file_path = os.path.join(CUR_DIR, f"{file_name}.{ext}") 26 | if os.path.isfile(file_path): 27 | with open(file_path, mode="r", encoding="utf-8") as doc_file: 28 | return doc_file.read(), ctype 29 | 30 | 31 | CHANGE, CHANGE_CTYPE = read_doc_file("CHANGES", "CHANGELOG") 32 | README, README_CTYPE = read_doc_file("README") 33 | LONG_DESCRIPTION = LONG_DESCRIPTION_CTYPE = None 34 | if CHANGE or README: 35 | LONG_DESCRIPTION = f"{README}\n\n{CHANGE}" 36 | LONG_DESCRIPTION_CTYPE = README_CTYPE or CHANGE_CTYPE 37 | 38 | 39 | def _parse_requirements(file_path, requirements, links): 40 | # type: (str, Set[str], Set[str]) -> None 41 | """ 42 | Parses a requirements file to extra packages and links. 43 | 44 | :param file_path: file path to the requirements file. 45 | :param requirements: pre-initialized set in which to store extracted package requirements. 46 | :param links: pre-initialized set in which to store extracted link reference requirements. 47 | """ 48 | if not os.path.isfile(file_path): 49 | return 50 | with open(file_path, "r") as requirements_file: 51 | for line in requirements_file: 52 | # ignore empty line, comment line or reference to other requirements file (-r flag) 53 | if not line or line.startswith("#") or line.startswith("-"): 54 | continue 55 | if "git+https" in line: 56 | pkg = line.split("#")[-1] 57 | links.add(line.strip()) 58 | requirements.add(pkg.replace("egg=", "").rstrip()) 59 | elif line.startswith("http"): 60 | links.add(line.strip()) 61 | else: 62 | requirements.add(line.strip()) 63 | 64 | 65 | LOGGER.info("reading requirements") 66 | # See https://github.com/pypa/pip/issues/3610 67 | # use set to have unique packages by name 68 | LINKS = set() 69 | REQUIREMENTS = set() 70 | DOCS_REQUIREMENTS = set() 71 | TEST_REQUIREMENTS = set() 72 | _parse_requirements("requirements.txt", REQUIREMENTS, LINKS) 73 | _parse_requirements("requirements-doc.txt", DOCS_REQUIREMENTS, LINKS) 74 | _parse_requirements("requirements-dev.txt", TEST_REQUIREMENTS, LINKS) 75 | LINKS = list(LINKS) 76 | REQUIREMENTS = list(REQUIREMENTS) 77 | 78 | LOGGER.info("base requirements: %s", REQUIREMENTS) 79 | LOGGER.info("docs requirements: %s", DOCS_REQUIREMENTS) 80 | LOGGER.info("test requirements: %s", TEST_REQUIREMENTS) 81 | LOGGER.info("link requirements: %s", LINKS) 82 | 83 | AUTHORS = { 84 | "Francis Charette-Migneault": "francis.charette.migneault@gmail.com", 85 | } 86 | 87 | # this is automatically updated by 'make bump' 88 | VERSION = "0.2.0" 89 | 90 | setup( 91 | name="pystac-ml-aoi", 92 | version=VERSION, 93 | description="Implementation of pystac utilities for STAC ML-AOI extension.", 94 | long_description=LONG_DESCRIPTION, 95 | long_description_content_type=LONG_DESCRIPTION_CTYPE, 96 | classifiers=[ 97 | "Development Status :: 4 - Beta", 98 | "Environment :: Web Environment", 99 | "Framework :: Pydantic", 100 | "Intended Audience :: Developers", 101 | "Intended Audience :: Information Technology", 102 | "Intended Audience :: Science/Research", 103 | "License :: OSI Approved :: Apache Software License", 104 | "Natural Language :: English", 105 | "Operating System :: POSIX", 106 | "Programming Language :: Python", 107 | "Programming Language :: Python :: 3", 108 | "Programming Language :: Python :: 3.8", 109 | "Programming Language :: Python :: 3.9", 110 | "Programming Language :: Python :: 3.10", 111 | "Programming Language :: Python :: 3.11", 112 | "Programming Language :: Python :: 3.12", 113 | "Programming Language :: Python :: 3 :: Only", 114 | "Topic :: Internet :: WWW/HTTP", 115 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 116 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 117 | "Topic :: Scientific/Engineering :: Atmospheric Science", 118 | "Topic :: Scientific/Engineering :: GIS", 119 | "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator", 120 | "Topic :: System :: Distributed Computing", 121 | ], 122 | author=", ".join(AUTHORS.keys()), 123 | author_email=", ".join(AUTHORS.values()), 124 | url="https://github.com/stac-extensions/ml-aoi", 125 | download_url=f"https://github.com/stac-extensions/ml-aoi/archive/refs/tags/{VERSION}.zip", 126 | license="Apache License 2.0", 127 | keywords=" ".join([ 128 | "STAC", 129 | "pystac", 130 | "Machine Learning", 131 | "Area of Interest", 132 | "Annotation", 133 | "Geospatial", 134 | ]), 135 | packages=find_packages(), 136 | include_package_data=True, 137 | package_data={"": ["*.json"]}, 138 | zip_safe=False, 139 | test_suite="tests", 140 | python_requires=">=3.8, <4", 141 | install_requires=REQUIREMENTS, 142 | dependency_links=LINKS, 143 | extras_require={ 144 | "docs": DOCS_REQUIREMENTS, 145 | "dev": TEST_REQUIREMENTS, 146 | "test": TEST_REQUIREMENTS, 147 | }, 148 | entry_points={ 149 | } 150 | ) 151 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import cast 3 | 4 | import pystac 5 | import pytest 6 | 7 | from pystac_ml_aoi.extensions.ml_aoi import ML_AOI_SCHEMA_PATH, ML_AOI_SCHEMA_URI 8 | 9 | 10 | @pytest.fixture(scope="session", name="stac_validator", autouse=True) 11 | def make_stac_ml_aoi_validator( 12 | request: pytest.FixtureRequest, 13 | ) -> pystac.validation.stac_validator.JsonSchemaSTACValidator: 14 | """ 15 | Update the :class:`pystac.validation.RegisteredValidator` with the local ML-AOI JSON schema definition. 16 | 17 | Because the schema is *not yet* uploaded to the expected STAC schema URI, 18 | any call to :func:`pystac.validation.validate` or :meth:`pystac.stac_object.STACObject.validate` results 19 | in ``GetSchemaError`` when the schema retrieval is attempted by the validator. By adding the schema to the 20 | mapping beforehand, remote resolution can be bypassed temporarily. 21 | """ 22 | validator = pystac.validation.RegisteredValidator.get_validator() 23 | validator = cast(pystac.validation.stac_validator.JsonSchemaSTACValidator, validator) 24 | validation_schema = json.loads(pystac.StacIO.default().read_text(ML_AOI_SCHEMA_PATH)) 25 | validator.schema_cache[ML_AOI_SCHEMA_URI] = validation_schema 26 | pystac.validation.RegisteredValidator.set_validator(validator) # apply globally to allow 'STACObject.validate()' 27 | return validator 28 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Tests all files in the `examples` directory against the JSON-schema. 5 | """ 6 | import glob 7 | import os 8 | 9 | import pystac 10 | import pytest 11 | 12 | from pystac_ml_aoi.extensions.ml_aoi import ML_AOI_SCHEMA_URI 13 | 14 | CUR_DIR = os.path.dirname(os.path.realpath(__file__)) 15 | ROOT_DIR = os.path.dirname(CUR_DIR) 16 | EXAMPLES_DIR = os.path.join(ROOT_DIR, "examples") 17 | 18 | 19 | @pytest.mark.parametrize("file_path", glob.glob(f"{EXAMPLES_DIR}/**/collection_*.json", recursive=True)) 20 | def test_stac_collection_examples( 21 | file_path: str, 22 | stac_validator: pystac.validation.stac_validator.JsonSchemaSTACValidator, 23 | ) -> None: 24 | col = pystac.Collection.from_file(file_path) 25 | valid_schemas = col.validate(validator=stac_validator) 26 | assert ML_AOI_SCHEMA_URI in valid_schemas 27 | 28 | 29 | @pytest.mark.parametrize("file_path", glob.glob(f"{EXAMPLES_DIR}/**/item_*.geojson", recursive=True)) 30 | def test_stac_item_examples( 31 | file_path: str, 32 | stac_validator: pystac.validation.stac_validator.JsonSchemaSTACValidator, 33 | ) -> None: 34 | item = pystac.Item.from_file(file_path) 35 | valid_schemas = item.validate(validator=stac_validator) 36 | assert ML_AOI_SCHEMA_URI in valid_schemas 37 | -------------------------------------------------------------------------------- /tests/test_pystac_extension.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Test functionalities provided by :class:`MLAOI_Extension`. 5 | """ 6 | import unittest 7 | from typing import cast 8 | 9 | import pystac 10 | import pytest 11 | import shapely 12 | from dateutil.parser import parse as dt_parse 13 | from pystac.extensions.label import LabelExtension, LabelTask, LabelType 14 | 15 | from pystac_ml_aoi.extensions.ml_aoi import ( 16 | ML_AOI_SCHEMA_URI, 17 | ML_AOI_Extension, 18 | ML_AOI_ItemExtension, 19 | ML_AOI_Role, 20 | ML_AOI_Split 21 | ) 22 | 23 | EUROSAT_EXAMPLE_BASE_URL = "https://raw.githubusercontent.com/ai-extensions/stac-data-loader/0.5.0/data/EuroSAT" 24 | EUROSAT_EXAMPLE_ASSET_ITEM_URL = ( 25 | f"{EUROSAT_EXAMPLE_BASE_URL}/stac/subset/train/item-42.json" 26 | ) 27 | EUROSAT_EXAMPLE_ASSET_LABEL_URL = ( 28 | EUROSAT_EXAMPLE_BASE_URL + 29 | "/data/subset/ds/images/remote_sensing/otherDatasets/sentinel_2/label/Residential/Residential_1331.geojson" 30 | ) 31 | EUROSAT_EXAMPLE_ASSET_RASTER_URL = ( 32 | EUROSAT_EXAMPLE_BASE_URL + 33 | "/data/subset/ds/images/remote_sensing/otherDatasets/sentinel_2/tif/Residential/Residential_1331.tif" 34 | ) 35 | 36 | 37 | @pytest.fixture(scope="function", name="collection") 38 | def make_base_stac_collection() -> pystac.Collection: 39 | """ 40 | Generates a sample STAC Collection with preloaded extensions relevant for testing ML-AOI. 41 | 42 | Example reference STAC Collection taken from: 43 | 44 | - https://github.com/stac-extensions/ml-aoi/blob/main/examples/collection_EuroSAT-subset-train.json 45 | - https://github.com/ai-extensions/stac-data-loader/blob/main/data/EuroSAT/stac/subset/train/collection.json 46 | 47 | The ML-AOI fields are to be extended by the various unit test functions. 48 | """ 49 | ext = pystac.Extent( 50 | spatial=pystac.SpatialExtent( 51 | bboxes=[[-7.882190080512502, 37.13739173208318, 27.911651652899923, 58.21798141355221]], 52 | ), 53 | temporal=pystac.TemporalExtent( 54 | intervals=[dt_parse("2015-06-27T10:25:31.456Z"), dt_parse("2017-06-14T00:00:00Z")], 55 | ), 56 | ) 57 | col = pystac.Collection( 58 | id="EuroSAT-subset-train", 59 | description=( 60 | "EuroSAT dataset with labeled annotations for land-cover classification and associated imagery. " 61 | "This collection represents samples part of the train split set for training machine learning algorithms." 62 | ), 63 | extent=ext, 64 | license="MIT", 65 | catalog_type=pystac.CatalogType.ABSOLUTE_PUBLISHED, 66 | ) 67 | return col 68 | 69 | 70 | @pytest.fixture(scope="function", name="item") 71 | def make_base_stac_item() -> pystac.Item: 72 | # pylint: disable=line-too-long 73 | """ 74 | Generates a sample STAC Item with preloaded extensions relevant for testing ML-AOI. 75 | 76 | Example reference STAC Item taken from: 77 | 78 | - https://github.com/stac-extensions/ml-aoi/blob/main/examples/item_EuroSAT-subset-train-sample-42-class-Residential.geojson 79 | - https://github.com/ai-extensions/stac-data-loader/blob/main/data/EuroSAT/stac/subset/train/item-42.json 80 | 81 | The ML-AOI fields are to be extended by the various unit test functions. 82 | """ 83 | geom = { 84 | "type": "Polygon", 85 | "coordinates": [ 86 | [ 87 | [ 88 | -3.1380012709408427, 89 | 51.531997746547134 90 | ], 91 | [ 92 | -3.1380012709408427, 93 | 51.53774085855159 94 | ], 95 | [ 96 | -3.1472537450112785, 97 | 51.53774085855159 98 | ], 99 | [ 100 | -3.1472537450112785, 101 | 51.531997746547134 102 | ], 103 | [ 104 | -3.1380012709408427, 105 | 51.531997746547134 106 | ] 107 | ] 108 | ] 109 | } 110 | bbox = list(shapely.geometry.shape(geom).bounds) 111 | asset_label = pystac.Asset( 112 | href=EUROSAT_EXAMPLE_ASSET_LABEL_URL, 113 | media_type=pystac.MediaType.GEOJSON, 114 | roles=["data"], 115 | extra_fields={ 116 | "ml-aoi:role": ML_AOI_Role.LABEL, 117 | } 118 | ) 119 | asset_raster = pystac.Asset( 120 | href=EUROSAT_EXAMPLE_ASSET_RASTER_URL, 121 | media_type=pystac.MediaType.GEOTIFF, 122 | roles=["data"], 123 | extra_fields={ 124 | "ml-aoi:reference-grid": True, 125 | "ml-aoi:role": ML_AOI_Role.FEATURE, 126 | } 127 | ) 128 | item = pystac.Item( 129 | id="EuroSAT-subset-train-sample-42-class-Residential", 130 | geometry=geom, 131 | bbox=bbox, 132 | start_datetime=dt_parse("2015-06-27T10:25:31.456Z"), 133 | end_datetime=dt_parse("2017-06-14T00:00:00Z"), 134 | datetime=None, 135 | properties={}, 136 | assets={"label": asset_label, "raster": asset_raster}, 137 | ) 138 | label_item = LabelExtension.ext(item, add_if_missing=True) 139 | label_item.apply( 140 | label_description="ml-aoi-test", 141 | label_type=LabelType.VECTOR, 142 | label_tasks=[LabelTask.CLASSIFICATION, LabelTask.SEGMENTATION], 143 | label_properties=["class"], 144 | ) 145 | return item 146 | 147 | 148 | def test_ml_aoi_pystac_collection_with_apply_method( 149 | collection: pystac.Collection, 150 | stac_validator: pystac.validation.stac_validator.JsonSchemaSTACValidator, 151 | ) -> None: 152 | """ 153 | Validate extending a STAC Collection with ML-AOI extension. 154 | """ 155 | assert ML_AOI_SCHEMA_URI not in collection.stac_extensions 156 | ml_aoi_col = ML_AOI_Extension.ext(collection, add_if_missing=True) 157 | ml_aoi_col.apply(split=[ML_AOI_Split.TRAIN]) 158 | assert ML_AOI_SCHEMA_URI in collection.stac_extensions 159 | collection.validate() 160 | ml_aoi_col_json = collection.to_dict() 161 | assert ml_aoi_col_json["summaries"] == {"ml-aoi:split": ["train"]} 162 | 163 | 164 | def test_ml_aoi_pystac_collection_with_field_property( 165 | collection: pystac.Collection, 166 | stac_validator: pystac.validation.stac_validator.JsonSchemaSTACValidator, 167 | ) -> None: 168 | """ 169 | Validate extending a STAC Collection with ML-AOI extension. 170 | """ 171 | assert ML_AOI_SCHEMA_URI not in collection.stac_extensions 172 | ml_aoi_col = ML_AOI_Extension.ext(collection, add_if_missing=True) 173 | ml_aoi_col.split = [ML_AOI_Split.TRAIN] 174 | assert ML_AOI_SCHEMA_URI in collection.stac_extensions 175 | collection.validate() 176 | ml_aoi_col_json = collection.to_dict() 177 | assert ml_aoi_col_json["summaries"] == {"ml-aoi:split": ["train"]} 178 | 179 | 180 | def test_ml_aoi_pystac_collection_self_link_role( 181 | collection: pystac.Collection, 182 | stac_validator: pystac.validation.stac_validator.JsonSchemaSTACValidator, 183 | ) -> None: 184 | """ 185 | Validate extending a STAC Collection with ML-AOI extension. 186 | """ 187 | assert ML_AOI_SCHEMA_URI not in collection.stac_extensions 188 | ml_aoi_col = ML_AOI_Extension.ext(collection, add_if_missing=True) 189 | ml_aoi_col.split = [ML_AOI_Split.TEST] 190 | assert ML_AOI_SCHEMA_URI in collection.stac_extensions 191 | ml_aoi_col.set_self_href("https://example.com/collections/test") 192 | ml_aoi_col_json = collection.to_dict() 193 | assert ml_aoi_col_json["summaries"] == {"ml-aoi:split": ["test"]} 194 | ml_aoi_col_links = ml_aoi_col_json["links"] 195 | assert any(link == { 196 | "rel": "self", 197 | "href": "https://example.com/collections/test", 198 | "type": "application/json", 199 | "ml-aoi:split": "test", 200 | } for link in ml_aoi_col_links), ml_aoi_col_links 201 | 202 | 203 | def test_ml_aoi_pystac_item_with_apply_method( 204 | item: pystac.Item, 205 | stac_validator: pystac.validation.stac_validator.JsonSchemaSTACValidator, 206 | ) -> None: 207 | """ 208 | Validate extending a STAC Collection with ML-AOI extension. 209 | """ 210 | assert ML_AOI_SCHEMA_URI not in item.stac_extensions 211 | ml_aoi_item = ML_AOI_Extension.ext(item, add_if_missing=True) 212 | ml_aoi_item.apply(split=ML_AOI_Split.TRAIN) 213 | assert ML_AOI_SCHEMA_URI in item.stac_extensions 214 | item.validate() 215 | ml_aoi_item_json = item.to_dict() 216 | assert ml_aoi_item_json["properties"]["ml-aoi:split"] == "train" 217 | 218 | 219 | def test_ml_aoi_pystac_item_with_field_property( 220 | item: pystac.Item, 221 | stac_validator: pystac.validation.stac_validator.JsonSchemaSTACValidator, 222 | ) -> None: 223 | """ 224 | Validate extending a STAC Collection with ML-AOI extension. 225 | """ 226 | assert ML_AOI_SCHEMA_URI not in item.stac_extensions 227 | ml_aoi_item = ML_AOI_Extension.ext(item, add_if_missing=True) 228 | ml_aoi_item.split = ML_AOI_Split.TRAIN 229 | assert ML_AOI_SCHEMA_URI in item.stac_extensions 230 | item.validate() 231 | ml_aoi_item_json = item.to_dict() 232 | assert ml_aoi_item_json["properties"]["ml-aoi:split"] == "train" 233 | 234 | 235 | def test_ml_aoi_pystac_item_filter_assets( 236 | item: pystac.Item, 237 | stac_validator: pystac.validation.stac_validator.JsonSchemaSTACValidator, 238 | ) -> None: 239 | """ 240 | Validate extending a STAC Collection with ML-AOI extension. 241 | """ 242 | assert ML_AOI_SCHEMA_URI not in item.stac_extensions 243 | ml_aoi_item = cast(ML_AOI_ItemExtension, ML_AOI_Extension.ext(item, add_if_missing=True)) 244 | ml_aoi_item.split = ML_AOI_Split.TRAIN 245 | assets = ml_aoi_item.get_assets() 246 | assert len(assets) == 2 and list(assets) == ["label", "raster"] 247 | assets = ml_aoi_item.get_assets(reference_grid=True) 248 | assert len(assets) == 1 and "raster" in assets 249 | assets = ml_aoi_item.get_assets(role=ML_AOI_Role.LABEL) 250 | assert len(assets) == 1 and "label" in assets 251 | 252 | 253 | if __name__ == "__main__": 254 | unittest.main() 255 | --------------------------------------------------------------------------------