-
Notifications
You must be signed in to change notification settings - Fork 1
Repositorio de imágenes #5
Comments
Si creo una clase Image que gestione abrir/cerrar las imágenes, la entidad Badge tendrá una dependencia con esa clase asi que para no rompera la arquitectura las alternativas que veo son:
|
On Sun, 28 Jul 2019 at 18:03, Martín ***@***.***> wrote:
Una opción un poco mejor es crear una clase Image que guarde o un StringIO
o la ruta al archivo. Si se intenta leer o escribir y está en modo
StringIO, se hace la operación. Pero si se detecta que es una ruta, se abre
el archivo, y luego se realiza la operación correspondiente sobre el
archivo abierto.
Bueno, en la arquitectura hexagonal, dentro del dominio a veces conviene
meter otro tipo de objetos. Se llaman Value object
https://en.wikipedia.org/wiki/Value_object
https://martinfowler.com/bliki/ValueObject.html
Una entidad es un objeto que queremos identificar de forma única con un id.
Dos entidades son la misma si tienen el mismo id. Y podemos recuperar una
entidad a partir de un id.
Pero a veces necesitamos otras clases de ayuda que no necesitan un id.
Simplemente dos objetos son el mismo si tienen lo mismo valores.
Por ejemplo, si la entidad Badge tuviera que guardar un vector de 2D ¿tiene
sentido que la clase Vector2D sea una entidad? Lo cierto es que no.
Cualquier vector es igual a otro vector solo si sus coordenada son las
misma. No son entidades.
En este caso se puede hacer algo parecido. Una imagen puede ser una
entidad, con su id. Si lo viéramos en términos de bases de datos sería que
las imágenes tienen su propia tabla.
O una imagen puede ser un Value Object, es decir, un valor de Badges, una
columna más de la tabla de Badges, si lo vemos como base de datos. Como un
string. Lo que pasa es que Python tiene ya una clase str para las cadena
pero ¿tiene una clase para las imágenes?
Podríamos crear nuestra propia clase a lo Value Object, peor primero hay
que pensar lo que queremos.
Queremos manejar una imagen como un archivo:
https://docs.python.org/3/library/io.html#module-io
no nos interesa nada más de una imagen. No queremos ponerle otros atributos
que no sean los de una archivo.
Así que el File-like object que devuelve open() nos sirve solo que:
a) Cuando se está creando un badge no existe el archivo aun. En ese caso
podemos poner una instancia de ByteIO, que simula un archivo sobre una
array de bytes (los bytes de la imagen que llegaron por la request).
b) Cuando hemos recuperado un badge tenemos un archivo con la imagen pero
no queremos abrir el archivo solo porque hemos recuperado el badge. Mejor
abrir cuando queramos leer los datos.
Hay varias opciones. Una sencilla es que el atributo image guarde una URL.
Si es una url tipo file:/// es una ruta a un archivo local si es data:...
es que contiene los datos ahí mismo.
Para facilitar el archivo al os bytes badgeService es de ayuda. Primero
badgeService.create() debe poder recibir los bytes de la imagen (que te
llegan por el la petición HTTP) y guarda en image la URL como data:...
puesto que en ese caso aun no hay archivo.
badgeService puede tener métodos como badgeService.open_image(badge, mode)
que devuelven un file-like object de la imagen. Si la URL era file:/// era
solo abrir el archivo y devolver le objeto.
Si la URL es data:.. hay que quitar lo el data:, crear un byteIO con el
resto y devolverlo.
Luego el repository solo debe considerar un caso especial, el salvado
cuando la URL es data:. Ahí debe coger los datos, guardarlos en un archivo
y precuparse que en el json acabe la ruta del archivo como file:///... para
que todo encaje.
Como digo hay otras soluciones, como que Badges.image no sea una cadena con
al URL si no un método que al llamarlo devuelve el file-like object. No
termina de gustarme.
La más programación orientada a objetos es que Badges.image guarde una
instancia de una nueva clase Image que se comporta como un file-like
object. Como hacer un file-like object es un rollo lo suyo sería usar las
facilidades del paquete filelike. Dentro Image o tiene un path o un ByteIO.
Si tiene un path, cuando le piden la primera operación de E/S abre el
archivo y le hace la petición. Si tiene un byteIO puede mandarle la
petición directamente.
Vamos que Image es un wrapper qe se comportaría como un archivo abierto
cualquiera. En el salvado del Badge el repositorio y debe ser listo y solo
hacer algo si Image es de los que contienen ByteIO.
Esta es la solución que pensé cuando hice el comentario pero cuanto más lo
pienso más me gusta guarda la URL y añadir el open_image() a badgeService.
Como se ve, usando services, no hay que inyectar nada más.
… Si creo una clase Image que gestione abrir/cerrar las imágenes, la entidad
Badge tendrá una dependencia con esa clase asi que para no rompera la
arquitectura las alternativas que veo son:
1. Inyección de dependencias
2. La clase Imagen es una entidad
¿Cuál es la mejor? A mí me parece más facil la segunda.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#5>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AACBBVAOA642DKQZ7HM22YLQBXGN3ANCNFSM4IGSUNIA>
.
|
Probé hacer una clase Image que gestionara abrir/cerrar la imagen y luego en la entidad Badge con el decorador @Property hacer que cuando se accediara a Badge.image llamar a Image.get_value(), pero vi que era mucho más simple hacer el método open_image(). Siempre paso lo que llega por internet, sea base64 o una url a un objeto BytesIO y luego creo la medalla. Creo que es más simple que guardar el string base64 en el json de la medalla y luego estar comprobando si es un file:// o un data:..., además debería ocupar menos espacio en disco. |
Hola
On Wed, 31 Jul 2019 at 14:58, Martín ***@***.***> wrote:
Probé hacer una clase Image que gestionara abrir/cerrar la imagen y luego
en la entidad Badge con el decorador @Property
<https://github.com/Property> hacer que cuando se accediara a Badge.image
llamar a Image.get_value(), pero vi que era mucho más simple hacer el
método open_image().
Lo del property era buena idea. Pero eso a mi me parece mejor y más
flexible.
La pega del property es que si accedes a Badges.image imagino que el getter
de la propiedad debe abrir el archivo y devolverte todo su contenido. Está
bien, pero no te permite controlar como quieres leer la imagen. Quizás no
la quieres toda sino solo la cabecera, para saber qué tipo de imagen es y
sus propiedades. Quizás la vas a transferir pero se manda en bloques de
tamaño N. ¿Por qué no leerla del archivo según se va enviando?.
En realidad como las medallas son pequeñas no es problema leerlas todas.
Pero me parece mucho mejor que con open_image obtengas algo que parece un
file-like object que puedes gestionar como un archivo.
En EntityJsonRepository, cuando se guarda una medalla si detecta que
Badge.image es un BytesIO, lo guarda en un archivo separado y pone en
Badge.image el "file://..." con la ruta del archivo.
O sea que Badge.image es un ByteIO o un str ¿no?
O Badge.image contiene una objecto clase Image que puede tener el ByteIO o
el la str con la URL como miembros privados?
Fijate que en como estamos en Python puedes hacer lo primero, porque las
variables pueden ser de un tipo u otro de forma dinámica. Ahora ByteIO,
mañana str. Te has ahorrado mucho trabajo.
Pero piensa que eso no lo puedes hacer con C++, Java, C# cualquier otro
lenguaje de tipado estático. No te quedaría otra que tener una clase Image
porque el tipo de Badges.image debería ser uno único siempre.
Algo así en pseudocódigo para esos lenguajes
class Image
{
virtual File open(mode) = 0;
}
class Badges
{
...
Image image;
}
Image tiene un método open para dar acceso al archivo de la imagen. El tema
es que hay dos tipos de Image, las que realmente tienen detrás un archivo y
las que los datos ya están en la memoria. Así que se deriva para tener dos
versiones de Imagen:
class ByteStreamImage : Image
{
ByteIO data;
overwrite File open(mode) {
return data;
}
}
class FileImage : Image
{
String url;
overwrite File open(mode) {
return open(url /* quitando lo de file:// */, mode);
}
}
Así Badges sería de la clase Imagen que puede ser realmente un FileImage o
un ByteStreamImage según si se usan los datos o la ruta del archivo.
Esta es para que veas todo lo que te has ahorrado. Claro, en Python es más
fácil cagarla y en Badges.image meter cualquier basura por error. Puedes
poner un entero (10) o cualquier objeto y el interprete no fallará hasta
que se ejecuta el código. Mientras que los compiladores de C++, Java y
otros si te avisarían si no asignas a Badges.image una imagen.
Asi no se cargan las imagenes en EntityJsonRepository.load, por ejemplo
cuando se usa el método BadgeService.name_exists para comprobar que no
existe el nombre de la medalla antes de crearla.
Para cargar la imagen hay que llamar a open_image, que comprueba si esta
abierta o no, y lo gestiona devolviendo un BytesIO.
Pero fíjate que no hace falta. Para devolver un BytesIO debes leer el
archivo completo con read() y meterlo en BytesIO. Eso no es necesario.
BytesIO está diseñado para comportarse como un archivo.
Es interesante cuando tienes ya los datos en la memoria pero quieres
acceder a ellos como un archivo y es una tontería crear un archivo, para
abrirlo, para pasar el file-like object a quien lo vaya a usar para que lea
los
datos de nuevo. Por eso existe BytesIO (o sstream en C++).
Yo creo que si Badges.image ya es un ByteIO, open_image debe devolver el
objeto BytesIO.
Pero si Badges.image es una str, lo que hay que hacer es abrir el archivo
con open() y devolver eso.
Ambos retornos adminten los mismos métodos. Son básicamente compatibles.
Quien use lo devuelto por open_image para leer o escribir no sabrá si lo
hace sobre un BytesIO o sobre un archivo de verdad.
Siempre paso lo que llega por internet, sea base64 o una url a un objeto
BytesIO y luego creo la medalla.
Perfecto. Si es una URL ¿qué haces? ¿Descargas el archivo al que apunta la
URL?
Creo que es más simple que guardar el string base64 en el json de la
medalla y luego estar comprobando si es un file:// o un data:..., además
debería ocupar menos espacio en disco.
Perfecto. La idea era precisamente nunca meter la imagen en el JSON sino
una URL al archivo. Se podía hacer pero eso solo es práctico para muy poco
datos. Como dices ocupa menos espacio en disco en binario y además te
aseguro que editar con un editor de texto un JSON que tiene 50Kb de datos
en Base64 que no vas a poder tocar, es un coñazo.
Saludos.
… —
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#5>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AACBBVEDC3EV6BLCWRNSQM3QCGLAZANCNFSM4IGSUNIA>
.
|
Si, es un atributo en donde unas veces pongo el BytesIO y otras el Path: @dataclass
class Badge:
id: EntityID
name: str
description: str
criteria: List[str]
image: Union[Path, str, BytesIO]
image_type: str
El problema es el create en api.py: # Crear medalla
self.badge_service.create(name=request_json['name'], description=request_json['description'],
criteria=request_json['criteria'], image=self.validated_image_bytes) Ahi le paso self.validated_image_bytes que es lo que saco decodificando # Crear medalla
image = Image(self.validated_image_bytes)
self.badge_service.create(name=request_json['name'],
description=request_json['description'],
criteria=request_json['criteria'], image=image) Según entendí las capas deberían ser independientes unas de otras, entonces eso no rompería la arquitectura? Estoy usando algo de la capa de dominio en la capa de infraestructura.
Esa era la idea, pero me falto una cosa. Lo acabo de modificar: def open_image(self, image: Union[str, BytesIO]) -> BytesIO:
'''
Método para leer la imagen. Si recibe un str abre la imagen,
si recibe un BytesIO es que la imagen ya estaba abierta y lo devuelve
'''
fileformat = 'file://'
if image.startswith(fileformat):
image = image.replace(fileformat, '')
with open(image, 'rb') as f:
image_bytes = BytesIO(f.read())
elif isinstance(image, BytesIO):
image_bytes = image
else:
image_bytes = None
raise TypeError(f'BadgeService.open_image: format not recognized')
return image_bytes |
On Thu, 1 Aug 2019 at 11:03, Martín ***@***.***> wrote:
O sea que Badge.image es un ByteIO o un str ¿no?
Caso A
O Badge.image contiene una objecto clase Image que puede tener el ByteIO o
el la str con la URL como miembros privados?
Caso B
Si, es un atributo en donde unas veces pongo el BytesIO y otras el Path:
@DataClass
class Badge:
id: EntityID
name: str
description: str
criteria: List[str]
image: Union[Path, str, BytesIO]
Sí. Para el Caso A
Para le Caso B:
image: Image
image_type: str
¿Esto para qué es?
Así Badges sería de la clase Imagen que puede ser realmente un FileImage o
un ByteStreamImage según si se usan los datos o la ruta del archivo.
El problema es el create en api.py:
# Crear medalla
self.badge_service.create(name=request_json['name'], description=request_json['description'],
criteria=request_json['criteria'], image=self.validated_image_bytes)
Ahi le paso self.validated_image_bytes que es lo que saco decodificando
request_json['image'], pero si la clase Badge tuviera un Image entonces
tendría que hacer algo como:
# Crear medalla
image = Image(self.validated_image_bytes)
self.badge_service.create(name=request_json['name'],
description=request_json['description'],
criteria=request_json['criteria'], image=image)
Según entendí las capas deberían ser independientes unas de otras,
entonces eso no rompería la arquitectura? Estoy usando algo de la capa de
dominio en la capa de infraestructura.
Exacto. Esto es Caso A.
En realidad es la idea es que las capas internas no dependen de las
externas. La capa de dominio no sabe nada de WebService ni del Repositorio.
Pero la de infraestructura si conoce las entidades. De hecho
badge_services.create devuelve una entidad.
Lo que pasa es que queremos que la lógica de negocio se implemente como en
los servicios de la capa de aplicación. Si un caso de uso es crear un
badge, hay un método de un servicio para crear badge. Ese método es
badge_service.create(). Pero si el objeto Imagen se crea fuera, es como si
la creación de badges tuviera una dependencia en que algo se hiciera por
fuera. Si mañana hubieran más componentes de infraestructura que quisieran
crear un badge, no les bastaría con llamar badge_service.create(). Hay un
detalle que todos deban hacer y no olvidar: crear Image.
Lo que puedes hacer es:
self.badge_service.create(name=request_json['name'],
description=request_json['description'],
criteria=request_json['criteria'], image=self.validated_image_bytes)
Y es el método create() el que crea la Imagen y la asigna al nuevo objeto
badge:
badge.image= Image(image)
Así la capa de infraestructura no sabe nada de como se crean los badges.
Solo que un dato que necesita badge_service.create es el array de bytes con
la imagen.
Ya dentro de create se hará con el lo que haga falta.
Yo creo que si Badges.image ya es un ByteIO, open_image debe devolver el
objeto BytesIO.
Pero si Badges.image es una str, lo que hay que hacer es abrir el archivo
con open() y devolver eso.
Ambos retornos adminten los mismos métodos. Son básicamente compatibles.
Quien use lo devuelto por open_image para leer o escribir no sabrá si lo
hace sobre un BytesIO o sobre un archivo de verdad.
Esa era la idea, pero me falto una cosa. Lo acabo de modificar:
def open_image(self, image: Union[str, BytesIO]) -> BytesIO:
Una cosa. No te obsesiones con los type hints.
En python las variables tiene tipo dinámico.
El caso de las dataclasses es relativamente nuevo. Como solo son grupos de
datos, es interesante indicar los tipos para que le constructor (generado
automáticamente) haga las comprobaciones pertinentes.
Pero eso es algo del otro día. En el resto del lenguaje los type hints no
son obligatorios ni el intérprete los usa para nada. ¿Para que sirve?
a) En proyectos grandes es una gran ayuda saber el tipo de lo que espera
una función. Además hay utilidades como mypy que se usan durante los test
para comprobar estáticamente si los tipos encajan.
b) El IDE si es listo los usa para avisarte si algo no encaja. Si no se
indican, el intenta suponer el tipo por los métodos de él que estás
llamando y los objetos que estás pasando. Y te puede dar avisos.
Vamos que open_image podría ser:
def open_image(self, image):
Por otro lado, como open_image es de BadgeServices, yo haría que recibiera
un badge. Es decir, que tu le dices, abre la imagen de un badge.
Ya dentro el accede a badge.image y hace lo que tenga que hacer. Es decir
que con type hint:
def open_image(self, badge: Badge) -> BufferedIOBase:
Como lo tienes descubres en el type hint un poco un detalle de la entidad
que a nadie debería preocupar. Que es que las imagenes pueden ser str o
BytesIO.
¿Por qué devolver BufferdIOBase? Porque tu quieres devolver algo que parece
un archivo abierto. Que realmente sea una archivo abierto o un BytesIO es
cosa de open_image().
Si dices que que devuelves un BytesIO, siempre debes devolver un BytesIO y
n podrías devolver lo que retorna open().
Si miras en :
https://docs.python.org/3/library/io.html#io.BufferedIOBase
Verás que BufferedIOBase es clase base de BytesIO y es lo más parecido a la
interfaz de un file-like object. Permite read y write y close().
BytesIO permite otros métodos como getbuffer o getvalues() que no tiene un
objeto devuelto por open().
Si dices que devuelves un BytesIO permite que quien llama a open_image
pueda querer usar esos métodos especiales de BytesIO, pero no siempre
podrá. No si devuelves un file object.
'''
Método para leer la imagen. Si recibe un str abre la imagen,
si recibe un BytesIO es que la imagen ya estaba abierta y lo devuelve
'''
fileformat = 'file://'
if image.startswith(fileformat):
image = image.replace(fileformat, '')
with open(image, 'rb') as f:
image_bytes = BytesIO(f.read())
esto es lo que digo. Ahora mismo lees el archivo para meterlo en un BytesIO
Pero ¿sabes lo que quiere hacer quien llama a open_imagen? ¿siempre querrá
leer el archivo completo?
De hecho open_images llama a la confusión. Habla de abrir la imagen pero
realmente abres y la lees.
Si eso es lo que debe hacer, quizás deba llamarse read_image y devolver
directamente f.read() o image. Es decir, los datos directamente.
Si la idea es devolver algo que es o parece un archivo para que el llamador
lo manipule como tal, open_image() está bien.
Pero entonces di:
return open(image, 'rb')
Y tan contento.
elif isinstance(image, BytesIO):
image_bytes = image
else:
image_bytes = None
Esto no hace falta. Nunca devolverás image_bytes porque justo despues
lanzas una excepción y el returno de más abajo jamás ocurrirá.
…
raise TypeError(f'BadgeService.open_image: format not recognized')
return image_bytes
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#5>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AACBBVGQUCD5NV2QBROVIDLQCKYHDANCNFSM4IGSUNIA>
.
|
Añadida clase BadgeImage y demás correcciones en #8 |
Cuando creas que la incidencia está resuelta la cierras indicando donde la resolviste. Otra opción es que un comentario de commit diga fix #5 o algo así. Y se cierra sola la incidencia al hacer el merge del PR. |
Al meter el almacenamiento de imágenes en WebServices surge el problema de que rompemos la arquitectura.
Ahora un adaptador sabe como se guardan cosas:
Ante eso existen dos soluciones.
1. Las imágenes son un atributo de los badges.
Durante la creación la imagen puede guardarse como un StringIO en el atributo image del badge y es el badge lo que manejamos. Cuando salvamos el badge, se guarda la imagen en un archivo aparte y en el json en el disco duro se indica su ruta.
Cuando se recupera un badge se puede, se puede abrir la image y poner el objeto File en el atributo imagen del badge recién recuperado.
El mayor pero es que se accede a los archivos de imagen y se abren aunque después no se vayan a usar.
Una opción un poco mejor es crear una clase Image que guarde o un StringIO o la ruta al archivo. Si se intenta leer o escribir y está en modo StringIO, se hace la operación. Pero si se detecta que es una ruta, se abre el archivo, y luego se realiza la operación correspondiente sobre el archivo abierto.
2. Imágenes como entidades.
Esto habría que pensarlo bien y no está claro que haga mucha falta.
The text was updated successfully, but these errors were encountered: