Compilar y empaquetar una aplicación de escritorio multiplataforma en Python

 

  Compilar y empaquetar una aplicación de escritorio multiplataforma en Python



En este tutorial le mostraremos, paso a paso, cómo crear una aplicación de calculadora en Python usando el marco Flet y empaquetarla como un ejecutable independiente para Windows, macOS y Linux, o implementarla como una aplicación web. La aplicación es un programa de consola simple, pero es una aplicación multiplataforma con una interfaz de usuario similar a la de la aplicación de calculadora de iPhone:

Descripción de la imagen

Puedes encontrar la demostración en vivo aquí.

En este tutorial, cubriremos todos los conceptos básicos para crear una aplicación de escritorio multiplataforma: creación de un diseño de página, adición de controles, control de eventos y opciones de empaquetado e implementación.

El tutorial consta de los siguientes pasos:

Paso 1: Primeros pasos con Flet

Para escribir una aplicación web de Flet no es necesario saber HTML, CSS o JavaScript, pero sí conocimientos básicos de Python y programación orientada a objetos.

Flet requiere Python 3.7 o superior. Para crear una aplicación web en Python con Flet, primero debe instalar el módulo:flet

pip install flet

Para empezar, vamos a crear una sencilla aplicación hello-world.

Crea con los siguientes contenidos:hello.py

import flet
from flet import Page, Text

def main(page: Page):
    page.add(Text(value="Hello, world!"))

flet.app(target=main)

Ejecute esta aplicación y verá una nueva ventana con un saludo:

Descripción de la imagen

Paso 2: Agregar controles de página

Ahora está listo para crear una aplicación de calculadora.

Para empezar, necesitará un control Text para mostrar el resultado del cálculo y algunos ElevatedButtons con todos los números y acciones en ellos.

Crea con los siguientes contenidos:calc.py

import flet
from flet import ElevatedButton, Page, Text

def main(page: Page):
    page.title = "Calc App"
    result = Text(value="0")
    page.add(
        result,
        ElevatedButton(text="AC"),
        ElevatedButton(text="+/-"),
        ElevatedButton(text="%"),
        ElevatedButton(text="/"),
        ElevatedButton(text="7"),
        ElevatedButton(text="8"),
        ElevatedButton(text="9"),
        ElevatedButton(text="*"),
        ElevatedButton(text="4"),
        ElevatedButton(text="5"),
        ElevatedButton(text="6"),
        ElevatedButton(text="-"),
        ElevatedButton(text="1"),
        ElevatedButton(text="2"),
        ElevatedButton(text="3"),
        ElevatedButton(text="+"),
        ElevatedButton(text="0"),
        ElevatedButton(text="."),
        ElevatedButton(text="="),
    )

flet.app(target=main)

Ejecute la aplicación y debería ver una página como esta:

Descripción de la imagen

Paso 3: Creación del diseño de la página

Ahora organicemos el texto y los botones en 6 filas horizontales.

Reemplace el contenido por lo siguiente:calc.py

import flet
from flet import ElevatedButton, Page, Row, Text

def main(page: Page):
    page.title = "Calc App"
    result = Text(value="0")
    page.add(
        Row(controls=[result]),
        Row(
            controls=[
                ElevatedButton(text="AC"),
                ElevatedButton(text="+/-"),
                ElevatedButton(text="%"),
                ElevatedButton(text="/"),
            ]
        ),
        Row(
            controls=[
                ElevatedButton(text="7"),
                ElevatedButton(text="8"),
                ElevatedButton(text="9"),
                ElevatedButton(text="*"),
            ]
        ),
        Row(
            controls=[
                ElevatedButton(text="4"),
                ElevatedButton(text="5"),
                ElevatedButton(text="6"),
                ElevatedButton(text="-"),
            ]
        ),
        Row(
            controls=[
                ElevatedButton(text="1"),
                ElevatedButton(text="2"),
                ElevatedButton(text="3"),
                ElevatedButton(text="+"),
            ]
        ),
        Row(
            controls=[
                ElevatedButton(text="0"),
                ElevatedButton(text="."),
                ElevatedButton(text="="),
            ]
        ),
    )

flet.app(target=main)

Ejecute la aplicación y debería ver una página como esta:

Descripción de la imagen

Uso de contenedores para la decoración

Para agregar un fondo negro con borde redondeado alrededor de la calculadora, usaremos el control Contenedor. El contenedor puede decorar solo un control, por lo que tendremos que envolver las 6 filas en una sola columna vertical que se usará como :content

Descripción de la imagen

Para completar la parte de la interfaz de usuario del programa, actualice y las propiedades del texto y las propiedades de los botones. Para una alineación uniforme de los botones dentro de las filas, usaremos la propiedad como se muestra en el diagrama anterior. colorsizecolorbgcolorexpand

Reemplace el contenido por lo siguiente:calc.py

import flet
from flet import (
    Column,
    Container,
    ElevatedButton,
    Page,
    Row,
    Text,
    border_radius,
    colors,
)

def main(page: Page):
    page.title = "Calc App"
    result = Text(value="0", color=colors.WHITE, size=20)
    page.add(
        Container(
            width=300,
            bgcolor=colors.BLACK,
            border_radius=border_radius.all(20),
            padding=20,
            content=Column(
                controls=[
                    Row(controls=[result], alignment="end"),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="AC",
                                bgcolor=colors.BLUE_GREY_100,
                                color=colors.BLACK,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="+/-",
                                bgcolor=colors.BLUE_GREY_100,
                                color=colors.BLACK,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="%",
                                bgcolor=colors.BLUE_GREY_100,
                                color=colors.BLACK,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="/",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="7",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="8",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="9",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="*",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="4",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="5",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="6",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="-",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="1",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="2",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="3",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="+",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="0",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=2,
                            ),
                            ElevatedButton(
                                text=".",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="=",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                ]
            ),
        )
    )

flet.app(target=main)

Run the app and you should see a page like this:

Image description

Just what we wanted!

Reusable UI components

While you can continue writing your app in the function, the best practice would be to create a reusable UI component. main

Imagine you are working on an app header, a side menu, or UI that will be a part of a larger project (for example, at Flet we will be using this Calculator app in a bigger "Gallery" app that will show all the examples for Flet framework).

Even if you can't think of such uses right now, we still recommend creating all your web apps with composability and reusability in mind.

To make a reusable Calc app component, we are going to encapsulate its state and presentation logic in a separate class.CalculatorApp

Replace contents with the following:calc.py

import flet
from flet import (
    Column,
    Container,
    ElevatedButton,
    Page,
    Row,
    Text,
    UserControl,
    border_radius,
    colors,
)

class CalculatorApp(UserControl):
    def build(self):
        result = Text(value="0", color=colors.WHITE, size=20)

        # application's root control (i.e. "view") containing all other controls
        return Container(
            width=300,
            bgcolor=colors.BLACK,
            border_radius=border_radius.all(20),
            padding=20,
            content=Column(
                controls=[
                    Row(controls=[result], alignment="end"),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="AC",
                                bgcolor=colors.BLUE_GREY_100,
                                color=colors.BLACK,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="+/-",
                                bgcolor=colors.BLUE_GREY_100,
                                color=colors.BLACK,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="%",
                                bgcolor=colors.BLUE_GREY_100,
                                color=colors.BLACK,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="/",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="7",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="8",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="9",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="*",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="4",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="5",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="6",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="-",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="1",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="2",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="3",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="+",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                    Row(
                        controls=[
                            ElevatedButton(
                                text="0",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=2,
                            ),
                            ElevatedButton(
                                text=".",
                                bgcolor=colors.WHITE24,
                                color=colors.WHITE,
                                expand=1,
                            ),
                            ElevatedButton(
                                text="=",
                                bgcolor=colors.ORANGE,
                                color=colors.WHITE,
                                expand=1,
                            ),
                        ]
                    ),
                ]
            ),
        )

def main(page: Page):
    page.title = "Calc App"
    # create application instance
    calc = CalculatorApp()

    # add application's root control to the page
    page.add(calc)

flet.app(target=main)

Read more about creating user controls.

Try something
Try adding two components to the page:
CalculatorApp

# create application instance
calc1 = CalculatorApp()
calc2 = CalculatorApp()

# add application's root control to the page
page.add(calc1, calc2)

Step 4: Handling events

Now let's make the calculator do its job. We will be using the same event handler for all the buttons and use property to differentiate between the actions depending on the button clicked. For each ElevatedButton control, specify event and set property equal to button's text, for example:dataon_click=self.button_clickeddata

ElevatedButton(
    text="AC",
    bgcolor=colors.BLUE_GREY_100,
    color=colors.BLACK,
    expand=1,
    on_click=self.button_clicked,
    data="AC",
)

Below is event handler that will reset the Text value when "AC" button is clicked:on_click

def button_clicked(self, e):
    if e.data == "AC":
        self.result.value = "0"

With similar approach, specify event and property for each button and add expected action to the event handler depending on value. Copy the entire code for this step from here.on_clickdatabutton_clickede.data

Run the app and see it in the action:

Image description

Step 5: Packaging as a desktop app

Congratulations! You have created your Calculator app with Flet, and it looks awesome! Now it's time to share your app with the world!

Flet Python app and all its dependencies can be packaged into an executable and user can run it on their computer without installing a Python interpreter or any modules.

PyInstaller is used to package Flet Python app and all its dependencies into a single package for Windows, macOS and Linux. To create Windows package, PyInstaller must be run on Windows; to build Linux app, it must be run on Linux; and to build macOS app - on macOS.

Start from installing PyInstaller:

pip install pyinstaller

Navigate to the directory where your file is located and build your app with the following command:.py

pyinstaller your_program.py

Your bundled Flet app should now be available in folder. Try running the program to see if it works.dist/your_program

On macOS/Linux:

./dist/your_program/your_program

on Windows:

dist\your_program\your_program.exe

Now you can just zip the contents of folder and distribute to your users! They don't need Python or Flet installed to run your packaged program - what a great alternative to Electron!dist/your_program

You'll notice though when you run a packaged program from macOS Finder or Windows Explorer a new console window is opened and then a window with app UI on top of it.

You can hide that console window by rebuilding the package with switch:--noconsole

pyinstaller your_program.py --noconsole --noconfirm

Bundling to one file

Contents of directory is an app executable plus supporting resources: Python runtime, modules, libraries.dist/your_program

You can package all these in a single executable by using switch:--onefile

pyinstaller your_program.py --noconsole --noconfirm --onefile

Obtendrá un ejecutable más grande en la carpeta. Ese ejecutable es un archivo autoejecutable con su programa y recursos de tiempo de ejecución que se desempaqueta en el directorio temporal cuando se ejecuta, es por eso que se tarda más en iniciar el paquete "onefile".dist

Nota:
Para macOS, puede distribuir uno u otro paquete de aplicaciones.
dist/your_programdist/your_program.app

Personalización del icono del paquete

El icono predeterminado de la aplicación del paquete es un disquete, lo que puede resultar confuso para los desarrolladores más jóvenes que se perdieron esos tiempos antiguos en los que se usaban disquetes para almacenar datos de la computadora.

Puede reemplazar el icono con el suyo propio agregando el argumento:--icon

pyinstaller your_program.py --noconsole --noconfirm --onefile --icon <your-image.png>

PyInstaller convertirá el PNG proporcionado a un formato específico de la plataforma (para Windows y para macOS), pero debe instalar el módulo Pillow para eso:.ico.icns

pip install pillow

Activos de embalaje

Tu aplicación Flet puede incluir activos. Los activos de la aplicación proporcionados están en la carpeta junto a que se pueden agregar a un paquete de aplicación con argumento, en macOS/Linux:assetsyour_program.py--add-data

pyinstaller your_program.py --noconsole --noconfirm --onefile --add-data "assets:assets"

En Windows debe estar delimitado con:assets;assets;

pyinstaller your_program.py --noconsole --noconfirm --onefile --add-data "assets;assets"

Nota:
Aquí puede encontrar todas las opciones de línea de comandos de PyInstaller

Uso de CI para el empaquetado multiplataforma

Para crear un paquete de aplicación con PyInstaller para un sistema operativo específico, debe ejecutarse en ese sistema operativo.

Si no tiene acceso a Mac o PC, puede agrupar su aplicación para las tres plataformas con AppVeyor - Servicio de integración continua para Windows, Linux y macOS. En resumen, la integración continua (CI) es un proceso automatizado de creación, prueba e implementación de aplicaciones (entrega continua - CD) en cada envío a un repositorio.

AppVeyor es gratuito para proyectos de código abierto alojados en GitHub, GitLab y Bitbucket. Para usar AppVeyor, inserte la aplicación en un repositorio dentro de uno de esos proveedores de control de código fuente.

Para comenzar con AppVeyor regístrese para obtener una cuenta gratuita.

Click "New project" button, authorize AppVeyor to access your GitHub, GitLab or Bitbucket account, choose a repository with your program and create a new project.

Now, to configure packaging of your app for Windows, Linux and macOS, add file with the following contents into the root of your repository . is a build configuration file, or CI workflow, describing build, test, packaging and deploy commands that must be run on every commit.appveyor.ymlappveyor.yml

Note:
You can just fork flet-dev/python-ci-example repository and customize it to your needs.

When you push any changes to GitHub repository, AppVeyor will automatically start a new build:

Image description

What that CI workflow does on every push to the repository:

  • Clones the repository to a clean virtual machine.
  • Installs app dependencies using .pip
  • Runs to package Python app into "onefile" bundle for WindowsmacOS and Ubuntu.pyinstaller
  • Zip/Tar packages and uploads them to "Artifacts".
  • Uploads packages to GitHub releases when a new tag is pushed. Just push a new tag to make a release!

GITHUB_TOKEN
in is a GitHub Personal Access Token (PAT) used by AppVeyor to publish created packages to repository "Releases". You need to generate your own token and replace it in . Login to your GitHub account and navigate to Personal access token page. Click "Generate new token" and select "public_repo" or "repo" scope for public or private repository respectively. Copy generated token to a clipboard and return to AppVeyor Portal. Navigate to Encrypt configuration data page and paste token to "Value to encrypt" field, click "Encrypt" button. Put encrypted value under in your .
GITHUB_TOKENappveyor.ymlappveyor.ymlGITHUB_TOKENappveyor.yml

Configure AppVeyor para su proyecto de Python, envíe una nueva etiqueta a un repositorio y obtenga "automáticamente" el paquete de escritorio para las tres plataformas en las versiones de GitHub. 🎉

Descripción de la imagen

Además de las versiones de GitHub, también puede configurar la liberación de artefactos en el bucket de Amazon S3 o en Azure Blob Storage.

Paso 6: Implementación de una aplicación web

La aplicación Flet se puede implementar como una aplicación web "independiente", lo que significa que tanto la aplicación Python como el servidor web Flet se implementan juntos como un paquete.

Las aplicaciones de Flet usan WebSockets para actualizaciones parciales en tiempo real de su interfaz de usuario y para enviar eventos de vuelta a su programa.
A la hora de elegir un proveedor de alojamiento para tu aplicación Flet, debes prestar atención a su compatibilidad con WebSockets. A veces, los WebSockets no están permitidos o forman parte de una oferta más costosa, a veces hay un proxy que interrumpe periódicamente la conexión de WebSocket por un tiempo de espera (Flet implementa la lógica de reconexión, pero de todos modos podría ser un comportamiento desagradable para los usuarios de la aplicación).

Otro factor importante a la hora de elegir un proveedor de alojamiento para la aplicación Flet es la latencia. Cada acción del usuario en la interfaz de usuario envía un mensaje a la aplicación Flet y la aplicación envía la interfaz de usuario actualizada al usuario. Asegúrese de que su proveedor de alojamiento tenga varios centros de datos, para que pueda ejecutar su aplicación más cerca de la mayoría de sus usuarios.

Nota:
No estamos afiliados a proveedores de alojamiento en esta sección, simplemente usamos su servicio y nos encanta.

Fly.io

Fly.io tiene una sólida compatibilidad con WebSocket y puede implementar la aplicación en un centro de datos cercano a los usuarios. Tienen precios muy atractivos con un generoso nivel gratuito que le permite alojar hasta 3 aplicaciones de forma gratuita.

Para empezar a usar Fly, instale flyctl y, a continuación, autentique:

flyctl auth login

Para implementar la aplicación, debe agregar los siguientes 3 archivos a la carpeta con su aplicación de Python.flyctl

Cree con una lista de dependencias de la aplicación. Como mínimo, debe contener el módulo:requirements.txtflet

flet>=0.1.33

Cree la aplicación Fly que describa:fly.toml

app = "<your-app-name>"

kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[env]
  FLET_SERVER_PORT = "8080"

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8080
  processes = ["app"]
  protocol = "tcp"
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

Reemplácelo con el nombre de la aplicación deseado que también se utilizará en la URL de la aplicación, como .<your-app-name>https://<your-app-name>.fly.dev

Tenga en cuenta que estamos estableciendo el valor de la variable de entorno en la que se ejecutará un puerto TCP interno de la aplicación web Flet.FLET_SERVER_PORT8080

Create que contiene los comandos para compilar el contenedor de la aplicación:Dockerfile

FROM python:3-alpine

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8080

CMD ["python", "./main.py"]

main.py es un archivo con su programa Python.

Nota:
Fly.io implementa todas las aplicaciones como un contenedor Docker, pero una gran ventaja de Fly es que proporciona un generador de Docker remoto gratuito, por lo que no necesita instalar Docker en su máquina.

A continuación, cambie la línea de comandos a una carpeta con la aplicación y ejecute el siguiente comando para crear e inicializar una nueva aplicación Fly:

flyctl apps create --name <your-app-name>

Para implementar la aplicación, ejecute:

flyctl deploy

¡Eso es todo! Para abrir la aplicación en el navegador, ejecute:

flyctl apps open

Replit

Replit es un IDE en línea y una plataforma de alojamiento para aplicaciones web escritas en cualquier idioma. Su nivel gratuito permite ejecutar cualquier número de aplicaciones con algunas limitaciones de rendimiento.

Para ejecutar tu app en Replit:

  • Regístrate en Replit.
  • Haga clic en el botón "Nuevo repl".
  • Seleccione el lenguaje "Python" de una lista y proporcione el nombre de repl, por ejemplo, .my-app
  • Haga clic en la pestaña "Paquetes" y busque el paquete; Seleccione su última versión.flet
  • Haga clic en la pestaña "Secretos" y agregue una variable con valor.FLET_SERVER_PORT5000
  • Vuelve a la pestaña "Archivos" y copia y pega tu aplicación en .main.py
  • Ejecuta la aplicación. Disfruta.

Resumen

En este tutorial has aprendido a:

  • Crea una aplicación Flet sencilla;
  • Trabajar con componentes de interfaz de usuario reutilizables;
  • Diseñe el diseño de la interfaz de usuario mediante y controles;ColumnRowContainer
  • Manejar eventos;
  • Empaqueta tu aplicación Flet en un ejecutable;
  • Implemente su aplicación Flet en la web;

Para obtener más información, puede explorar el repositorio de controles y ejemplos.

¡Nos encantaría escuchar sus comentarios! Envíanos un correo electrónico, únete a la discusión en Discord y síguenos en Twitter.

SHARE

Oscar perez

Arquitecto especialista en gestion de proyectos si necesitas desarrollar algun proyecto en Bogota contactame en el 3006825874 o visita mi pagina en www.arquitectobogota.tk

  • Image
  • Image
  • Image
  • Image
  • Image
    Blogger Comment
    Facebook Comment

0 comentarios:

Publicar un comentario