Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Table of Content](#table-of-content)
- [Summary of the project](#summary-of-the-project)
- [SPEC](#spec)
- [Logging Standard Django Management Commands](#logging-standard-django-management-commands)
- [Local development](#local-development)
- [Dependencies](#dependencies)
- [Python3.12](#python312)
Expand Down Expand Up @@ -53,6 +54,15 @@

See [SPEC](./spec/README.md)

## Logging Standard Django Management Commands

This project uses a modified `manage.py` that supports redirecting the output of the standard
Django management commands to the logger. For this, simply add `--redirect-std-to-logger`, e.g.:

```bash
app/manage.py migrate --redirect-std-to-logger
```

## Local development

### Dependencies
Expand Down
65 changes: 65 additions & 0 deletions app/helpers/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from contextlib import contextmanager
from contextlib import redirect_stderr
from contextlib import redirect_stdout
from io import StringIO
from logging import ERROR
from logging import INFO
from logging import getLogger
from logging.config import dictConfig
from time import time
from typing import Generator

from django.conf import settings


class TimestampedStringIO(StringIO):
""" A StringIO-like in-memory text buffer that logs each write and stores a timestamp for when
the content was appended.

"""

def __init__(self, level: int) -> None:
super().__init__()
self.level = level
self.messages: list[tuple[float, int, str]] = []

def write(self, s: str) -> int:
message = s.strip()
if message:
self.messages.append((time(), self.level, message))
return len(s)


@contextmanager
def redirect_std_to_logger(logger_name: str,
stderr_level: int = ERROR,
stdout_level: int = INFO) -> Generator[None, None, None]:
""" A context manager that redirects sys.stdout and sys.stderr to the logger using the given
levels.

Use it like this:

import sys
from utils.logging import redirect_std_to_logger

with redirect_std_to_logger('my_module'):
sys.out('This gets logged with level INFO')
sys.err('This gets logged with level ERROR')

"""

stderr = TimestampedStringIO(stderr_level)
stdout = TimestampedStringIO(stdout_level)
exception: Exception | None = None
with redirect_stderr(stderr), redirect_stdout(stdout):
try:
yield
except Exception as e: # pylint: disable=broad-exception-caught
exception = e

logger = getLogger(logger_name)
dictConfig(settings.LOGGING)
for _, level, message in sorted(stderr.messages + stdout.messages):
logger.log(level, message)
if exception:
logger.exception(exception)
8 changes: 7 additions & 1 deletion app/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path

from dotenv import load_dotenv
from helpers.logging import redirect_std_to_logger


def main():
Expand Down Expand Up @@ -41,4 +42,9 @@ def main():


if __name__ == '__main__':
main()
if '--redirect-std-to-logger' in sys.argv:
sys.argv.remove('--redirect-std-to-logger')
with redirect_std_to_logger(__name__):
main()
else:
main()
42 changes: 17 additions & 25 deletions app/stac_api/management/commands/calculate_extent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from django.db import connection

from stac_api.models.collection import Collection
from stac_api.utils import CommandHandler
from stac_api.utils import CustomBaseCommand


Expand All @@ -15,18 +14,30 @@ def boolean_input(question, default=None):
return len(result) > 0 and result[0].lower() == "y"


class Handler(CommandHandler):
class Command(CustomBaseCommand):
help = """Calculate the collection spacial and temporal extent for all collections that have
'extent_out_of_sync' set to true. After update, 'extent_out_of_sync' will be set to False.
This command is thought to be scheduled as cron job.
"""

def run(self):
def add_arguments(self, parser: CommandParser) -> None:
super().add_arguments(parser)
parser.add_argument(
'-a', '--all', action='store_true', help='Update extent for all collections'
)
parser.add_argument(
'-f', '--force', action='store_true', help='Run all without confirmation'
)

def handle(self, *args, **options):
self.print_success('running command to update collection extents')
# print(self.options)
qry = Collection.objects.filter(extent_out_of_sync=True)
if self.options['all']:
if options['all']:
qry = Collection.objects
collections = qry.values_list('id', flat=True)

# Prompt user to confirm update of all collections if force was not provided.
if self.options['all'] and not self.options['force']:
if options['all'] and not options['force']:
cont = boolean_input(
f"You are about to update {len(collections)} collections!\n" +
"Are you sure you want to continue? (y/n): ",
Expand Down Expand Up @@ -78,22 +89,3 @@ def run(self):
f"successfully updated extent of {len(collections)} collections",
extra={"duration": time.monotonic() - start}
)


class Command(CustomBaseCommand):
help = """Calculate the collection spacial and temporal extent for all collections that have
'extent_out_of_sync' set to true. After update, 'extent_out_of_sync' will be set to False.
This command is thought to be scheduled as cron job.
"""

def add_arguments(self, parser: CommandParser) -> None:
super().add_arguments(parser)
parser.add_argument(
'-a', '--all', action='store_true', help='Update extent for all collections'
)
parser.add_argument(
'-f', '--force', action='store_true', help='Run all without confirmation'
)

def handle(self, *args, **options):
Handler(self, options).run()
103 changes: 48 additions & 55 deletions app/stac_api/management/commands/dummy_asset.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,65 @@
import hashlib
import logging
import random
import uuid
from io import BytesIO

from django.conf import settings
from django.core.management.base import BaseCommand

from stac_api.utils import CommandHandler
from stac_api.utils import CustomBaseCommand
from stac_api.utils import get_s3_resource
from stac_api.utils import get_sha256_multihash
from stac_api.validators import MEDIA_TYPES

logger = logging.getLogger(__name__)

PREFIX = 'dummy-obj-'


class DummyAssetHandler(CommandHandler):
class Command(CustomBaseCommand):
help = f"""Upload dummy asset file on S3 for testing.

The command upload dummy asset file with random data on S3 for testing.
By default only one file is uploaded to
/{PREFIX}collection-1/{PREFIX}item-1/{PREFIX}asset-1.txt

Optionally you can create more than one asset on several items and collections.
If more than one asset is uploaded, then its extension is chosen randomly.

The asset uploaded is then printed to the console with its checksum:multishash.
"""

def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
'action',
type=str,
choices=['upload', 'clean'],
default='upload',
help='Define the action to be performed, either "upload" (default) to create and '
'upload dummy asset file or "clean" to delete them',
)

parser.add_argument(
'--collections',
type=int,
default=1,
help="Number of collections to create (default 1)"
)

parser.add_argument(
'--items',
type=int,
default=1,
help="Number of items per collection to create (default 1)"
)

parser.add_argument(
'--assets', type=int, default=1, help="Number of assets per item to create (default 1)"
)

def handle(self, *args, **options):
if options['action'] == 'clean':
self.clean()
elif options['action'] == 'upload':
self.upload()

def clean(self):
self.print_warning("Deleting all assets with prefix %s on S3...", PREFIX)
Expand Down Expand Up @@ -64,52 +106,3 @@ def upload(self):
self.print('%s,%s', file, get_sha256_multihash(content), level=2)
self.print('-' * 100, level=2)
self.print_success('Done')


class Command(BaseCommand):
help = f"""Upload dummy asset file on S3 for testing.

The command upload dummy asset file with random data on S3 for testing.
By default only one file is uploaded to
/{PREFIX}collection-1/{PREFIX}item-1/{PREFIX}asset-1.txt

Optionally you can create more than one asset on several items and collections.
If more than one asset is uploaded, then its extension is chosen randomly.

The asset uploaded is then printed to the console with its checksum:multishash.
"""

def add_arguments(self, parser):
parser.add_argument(
'action',
type=str,
choices=['upload', 'clean'],
default='upload',
help='Define the action to be performed, either "upload" (default) to create and '
'upload dummy asset file or "clean" to delete them',
)

parser.add_argument(
'--collections',
type=int,
default=1,
help="Number of collections to create (default 1)"
)

parser.add_argument(
'--items',
type=int,
default=1,
help="Number of items per collection to create (default 1)"
)

parser.add_argument(
'--assets', type=int, default=1, help="Number of assets per item to create (default 1)"
)

def handle(self, *args, **options):
handler = DummyAssetHandler(self, options)
if options['action'] == 'clean':
handler.clean()
elif options['action'] == 'upload':
handler.upload()
Loading
Loading